diff --git a/crates/arc-api/src/demo/mod.rs b/crates/arc-api/src/demo/mod.rs index a15ee7636..11464365d 100644 --- a/crates/arc-api/src/demo/mod.rs +++ b/crates/arc-api/src/demo/mod.rs @@ -2661,6 +2661,7 @@ mod settings { app_id: Some("12345".into()), client_id: Some("Iv1.abc123".into()), slug: Some("arc-dev".into()), + author: Default::default(), }, feature_flags: FeatureFlags { session_sandboxes: false, diff --git a/crates/arc-api/src/serve.rs b/crates/arc-api/src/serve.rs index 2e334b88c..75eda9d0e 100644 --- a/crates/arc-api/src/serve.rs +++ b/crates/arc-api/src/serve.rs @@ -136,7 +136,14 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow:: (auth_mode, client_auth, max_concurrent_runs) }; - let state = crate::server::create_app_state_with_options(db, factory, dry_run_mode, max_concurrent_runs); + let (git_author_name, git_author_email) = { + let cfg = shared_config.read().expect("config lock poisoned"); + ( + cfg.git.author.name.clone().unwrap_or_else(|| "arc".into()), + cfg.git.author.email.clone().unwrap_or_else(|| "arc@local".into()), + ) + }; + let state = crate::server::create_app_state_with_options(db, factory, dry_run_mode, max_concurrent_runs, git_author_name, git_author_email); crate::server::spawn_scheduler(Arc::clone(&state)); let router = build_router(state, auth_mode); diff --git a/crates/arc-api/src/server.rs b/crates/arc-api/src/server.rs index ab1432bbf..dd860004d 100644 --- a/crates/arc-api/src/server.rs +++ b/crates/arc-api/src/server.rs @@ -106,6 +106,8 @@ pub struct AppState { max_concurrent_runs: usize, scheduler_notify: tokio::sync::Notify, pub hook_config: arc_workflows::hook::HookConfig, + git_author_name: String, + git_author_email: String, } /// Build the axum Router with all run endpoints. @@ -365,7 +367,7 @@ pub fn create_app_state( db: sqlx::SqlitePool, registry_factory: impl Fn(Arc) -> HandlerRegistry + Send + Sync + 'static, ) -> Arc { - create_app_state_with_options(db, registry_factory, false, 5) + create_app_state_with_options(db, registry_factory, false, 5, "arc".into(), "arc@local".into()) } /// Create an `AppState` with the given database pool, registry factory, dry-run flag, and concurrency limit. @@ -374,6 +376,8 @@ pub fn create_app_state_with_options( registry_factory: impl Fn(Arc) -> HandlerRegistry + Send + Sync + 'static, dry_run: bool, max_concurrent_runs: usize, + git_author_name: String, + git_author_email: String, ) -> Arc { Arc::new(AppState { runs: Mutex::new(HashMap::new()), @@ -384,6 +388,8 @@ pub fn create_app_state_with_options( max_concurrent_runs, scheduler_notify: tokio::sync::Notify::new(), hook_config: arc_workflows::hook::HookConfig::default(), + git_author_name, + git_author_email, }) } @@ -579,6 +585,8 @@ async fn execute_run(state: Arc, run_id: String) { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: state.git_author_name.clone(), + git_author_email: state.git_author_email.clone(), }; let result = tokio::select! { @@ -1135,7 +1143,7 @@ mod tests { #[tokio::test] async fn test_model_dry_run_returns_ok() { - let state = create_app_state_with_options(test_db().await, test_registry, true, 5); + let state = create_app_state_with_options(test_db().await, test_registry, true, 5, "arc".into(), "arc@local".into()); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -1155,7 +1163,7 @@ mod tests { #[tokio::test] async fn test_model_dry_run_unknown_returns_404() { - let state = create_app_state_with_options(test_db().await, test_registry, true, 5); + let state = create_app_state_with_options(test_db().await, test_registry, true, 5, "arc".into(), "arc@local".into()); let app = build_router(state, AuthMode::Disabled); let req = Request::builder() @@ -1836,7 +1844,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn concurrency_limit_respected() { - let state = create_app_state_with_options(test_db().await, test_registry, false, 1); + let state = create_app_state_with_options(test_db().await, test_registry, false, 1, "arc".into(), "arc@local".into()); let app = test_app_with_scheduler(state); // Submit two runs with max_concurrent_runs=1 diff --git a/crates/arc-api/src/server_config.rs b/crates/arc-api/src/server_config.rs index ea958836d..830ed0b2f 100644 --- a/crates/arc-api/src/server_config.rs +++ b/crates/arc-api/src/server_config.rs @@ -65,6 +65,12 @@ pub enum GitProvider { Github, } +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] +pub struct GitAuthorConfig { + pub name: Option, + pub email: Option, +} + #[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize)] pub struct GitConfig { #[serde(default)] @@ -72,6 +78,8 @@ pub struct GitConfig { pub app_id: Option, pub client_id: Option, pub slug: Option, + #[serde(default)] + pub author: GitAuthorConfig, } #[derive(Debug, Clone, Deserialize, PartialEq, Serialize)] @@ -262,6 +270,34 @@ client_id = "Iv1.abc123" assert_eq!(config.git.provider, GitProvider::Github); assert_eq!(config.git.app_id, None); assert_eq!(config.git.client_id, None); + assert_eq!(config.git.author.name, None); + assert_eq!(config.git.author.email, None); + } + + #[test] + fn parse_git_author_config() { + let toml = r#" +[git.author] +name = "arc-bot" +email = "arc-bot@company.com" +"#; + let config: ServerConfig = toml::from_str(toml).unwrap(); + assert_eq!(config.git.author.name.as_deref(), Some("arc-bot")); + assert_eq!( + config.git.author.email.as_deref(), + Some("arc-bot@company.com") + ); + } + + #[test] + fn parse_git_author_partial() { + let toml = r#" +[git.author] +name = "custom-name" +"#; + let config: ServerConfig = toml::from_str(toml).unwrap(); + assert_eq!(config.git.author.name.as_deref(), Some("custom-name")); + assert_eq!(config.git.author.email, None); } #[test] diff --git a/crates/arc-cli/src/cli_config.rs b/crates/arc-cli/src/cli_config.rs index 82d660911..8b064d552 100644 --- a/crates/arc-cli/src/cli_config.rs +++ b/crates/arc-cli/src/cli_config.rs @@ -38,12 +38,19 @@ pub struct LlmDefaults { pub model: Option, } +#[derive(Clone, Debug, Default, Deserialize, PartialEq)] +pub struct CliGitConfig { + #[serde(default)] + pub author: arc_api::server_config::GitAuthorConfig, +} + #[derive(Clone, Debug, Default, Deserialize, PartialEq)] pub struct CliConfig { pub mode: Option, pub server: Option, pub agent: Option, pub llm: Option, + pub git: Option, } #[derive(Debug, PartialEq)] @@ -324,6 +331,25 @@ ca = "~/.arc/tls/ca.crt" assert_eq!(resolved.server_base_url, "https://cli.example.com"); } + #[test] + fn parse_git_author_config() { + let toml = r#" +[git.author] +name = "my-arc" +email = "me@local" +"#; + let config: CliConfig = toml::from_str(toml).unwrap(); + let git = config.git.unwrap(); + assert_eq!(git.author.name.as_deref(), Some("my-arc")); + assert_eq!(git.author.email.as_deref(), Some("me@local")); + } + + #[test] + fn parse_git_author_absent() { + let config: CliConfig = toml::from_str("").unwrap(); + assert_eq!(config.git, None); + } + #[test] fn resolve_mode_tls_from_config() { let tls = ClientTlsConfig { diff --git a/crates/arc-cli/src/main.rs b/crates/arc-cli/src/main.rs index 335e269a5..cae73040f 100644 --- a/crates/arc-cli/src/main.rs +++ b/crates/arc-cli/src/main.rs @@ -179,12 +179,28 @@ async fn main() -> Result<()> { let styles: &'static arc_util::terminal::Styles = Box::leak(Box::new(arc_util::terminal::Styles::detect_stderr())); let server_config = arc_api::server_config::load_server_config(None)?; + let cli_config = cli_config::load_cli_config(None)?; let github_app = build_github_app_credentials(&server_config); + + let cli_author = cli_config.git.as_ref().map(|g| &g.author); + let git_author_name = cli_author + .and_then(|a| a.name.as_deref()) + .or(server_config.git.author.name.as_deref()) + .unwrap_or("arc") + .to_string(); + let git_author_email = cli_author + .and_then(|a| a.email.as_deref()) + .or(server_config.git.author.email.as_deref()) + .unwrap_or("arc@local") + .to_string(); + arc_workflows::cli::run::run_command( args, server_config.run_defaults, styles, github_app, + git_author_name, + git_author_email, ) .await?; } diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index 89f946dd8..eb5f0c3b6 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -181,10 +181,12 @@ pub async fn run_command( run_defaults: RunDefaults, styles: &'static Styles, github_app: Option, + git_author_name: String, + git_author_email: String, ) -> anyhow::Result<()> { // Handle --run-branch resume: read everything from git metadata if let Some(branch) = args.run_branch.clone() { - return run_from_branch(args, &branch, styles).await; + return run_from_branch(args, &branch, styles, git_author_name, git_author_email).await; } let workflow_path = args @@ -695,6 +697,8 @@ pub async fn run_command( .collect(), checkpoint_exclude_globs, github_app: github_app.clone(), + git_author_name, + git_author_email, }; let run_start = Instant::now(); @@ -926,6 +930,8 @@ async fn run_from_branch( args: RunArgs, run_branch: &str, styles: &'static Styles, + git_author_name: String, + git_author_email: String, ) -> anyhow::Result<()> { // Extract run_id from branch name: "arc/run/{run_id}" -> "{run_id}" let run_id = run_branch @@ -1062,6 +1068,8 @@ async fn run_from_branch( labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name, + git_author_email, }; let run_start = Instant::now(); diff --git a/crates/arc-workflows/src/engine.rs b/crates/arc-workflows/src/engine.rs index 6e89bc498..97a288828 100644 --- a/crates/arc-workflows/src/engine.rs +++ b/crates/arc-workflows/src/engine.rs @@ -521,6 +521,8 @@ pub struct GitState { pub run_branch: Option, pub meta_branch: Option, pub checkpoint_exclude_globs: Vec, + pub git_author_name: String, + pub git_author_email: String, } /// How git checkpointing should be performed for a workflow run. @@ -542,6 +544,8 @@ pub async fn git_checkpoint_host( completed_count: usize, shadow_sha: Option, exclude_globs: Vec, + author_name: String, + author_email: String, ) -> Option { match tokio::task::spawn_blocking(move || { crate::git::checkpoint_commit( @@ -552,6 +556,8 @@ pub async fn git_checkpoint_host( completed_count, shadow_sha.as_deref(), &exclude_globs, + &author_name, + &author_email, ) }) .await @@ -587,6 +593,8 @@ pub async fn git_checkpoint_remote( completed_count: usize, shadow_sha: Option, exclude_globs: &[String], + author_name: &str, + author_email: &str, ) -> Option { // Stage everything (with optional excludes) let add_cmd = if exclude_globs.is_empty() { @@ -636,9 +644,9 @@ pub async fn git_checkpoint_remote( return None; } - // Commit with arc identity using the message file + // Commit with configured identity using the message file let commit_cmd = format!( - "{GIT_REMOTE} -c user.name=arc -c user.email=arc@local commit --allow-empty -F /tmp/arc-commit-msg" + "{GIT_REMOTE} -c user.name={author_name} -c user.email={author_email} commit --allow-empty -F /tmp/arc-commit-msg" ); let commit_result = sandbox .exec_command(&commit_cmd, 30_000, None, None, None) @@ -830,6 +838,10 @@ pub struct RunConfig { pub checkpoint_exclude_globs: Vec, /// GitHub App credentials for pushing metadata branches to origin. pub github_app: Option, + /// Git author name for checkpoint commits. + pub git_author_name: String, + /// Git author email for checkpoint commits. + pub git_author_email: String, } /// The workflow run execution engine. @@ -1208,6 +1220,8 @@ impl WorkflowRunEngine { run_branch: config.run_branch.clone(), meta_branch: config.meta_branch.clone(), checkpoint_exclude_globs: config.checkpoint_exclude_globs.clone(), + git_author_name: config.git_author_name.clone(), + git_author_email: config.git_author_email.clone(), })), _ => None, }; @@ -1258,7 +1272,11 @@ impl WorkflowRunEngine { None => None, }; if let Some(repo_path) = store_path { - let store = crate::git::MetadataStore::new(repo_path); + let store = crate::git::MetadataStore::new( + repo_path, + &config.git_author_name, + &config.git_author_email, + ); let manifest_bytes = serde_json::to_vec_pretty(&manifest).unwrap_or_default(); let dot_source = std::fs::read(config.logs_root.join("graph.dot")).unwrap_or_default(); @@ -1882,7 +1900,11 @@ impl WorkflowRunEngine { let repo_path = match mode { GitCheckpointMode::Host(ref p) | GitCheckpointMode::Remote(ref p) => p, }; - let store = crate::git::MetadataStore::new(repo_path); + let store = crate::git::MetadataStore::new( + repo_path, + &config.git_author_name, + &config.git_author_email, + ); serde_json::to_vec_pretty(&checkpoint) .ok() .and_then(|cp_json| { @@ -1931,6 +1953,8 @@ impl WorkflowRunEngine { completed_count, shadow_sha, config.checkpoint_exclude_globs.clone(), + config.git_author_name.clone(), + config.git_author_email.clone(), ) .await } @@ -1943,6 +1967,8 @@ impl WorkflowRunEngine { completed_count, shadow_sha, &config.checkpoint_exclude_globs, + &config.git_author_name, + &config.git_author_email, ) .await } @@ -2810,6 +2836,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -2833,6 +2861,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); let checkpoint_path = dir.path().join("checkpoint.json"); @@ -2864,6 +2894,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -2891,6 +2923,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -2914,6 +2948,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -2950,6 +2986,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3010,6 +3048,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3097,6 +3137,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3127,6 +3169,8 @@ mod tests { labels: HashMap::from([("env".into(), "test".into())]), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3153,6 +3197,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3179,6 +3225,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3208,6 +3256,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3365,6 +3415,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3405,6 +3457,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&g, &config).await.unwrap(); @@ -3463,6 +3517,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); @@ -3524,6 +3580,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; @@ -3589,6 +3647,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_ok()); @@ -3643,6 +3703,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3698,6 +3760,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); @@ -3728,6 +3792,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3754,6 +3820,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3779,6 +3847,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -3817,6 +3887,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; // Set cancel after a short delay (while the slow handler is running) @@ -3892,6 +3964,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3920,6 +3994,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3950,6 +4026,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -3985,6 +4063,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4018,6 +4098,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4048,6 +4130,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4139,6 +4223,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; // The engine returns Err because the Fail outcome has no outgoing fail edge, @@ -4345,6 +4431,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4378,6 +4466,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4418,6 +4508,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4498,6 +4590,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4589,6 +4683,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&g, &config).await; assert!(result.is_err()); @@ -4657,6 +4753,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -4712,6 +4810,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&g, &config).await.unwrap(); assert_eq!(outcome.status, StageStatus::Success); @@ -4768,6 +4868,8 @@ mod tests { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; 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 54fc93f81..cd2eadca2 100644 --- a/crates/arc-workflows/src/git.rs +++ b/crates/arc-workflows/src/git.rs @@ -145,6 +145,8 @@ pub fn checkpoint_commit( completed_count: usize, shadow_sha: Option<&str>, excludes: &[String], + author_name: &str, + author_email: &str, ) -> Result { tracing::debug!(path = %work_dir.display(), node_id, "Creating git checkpoint commit"); // Stage everything (with optional excludes) @@ -183,13 +185,15 @@ pub fn checkpoint_commit( } let message = trailerlink::format_message(&subject, "", &trailers); - // Commit with arc identity (works even if user.name/email not configured) + // Commit with configured identity (works even if user.name/email not configured) + let name_cfg = format!("user.name={author_name}"); + let email_cfg = format!("user.email={author_email}"); let output = git_cmd(work_dir) .args([ "-c", - "user.name=arc", + &name_cfg, "-c", - "user.email=arc@local", + &email_cfg, "commit", "--allow-empty", "-m", @@ -283,12 +287,16 @@ pub fn sanitize_ref_component(s: &str) -> String { /// (`arc/{run_id}`) so that runs can be resumed from git alone. pub struct MetadataStore { repo_path: std::path::PathBuf, + author_name: String, + author_email: String, } impl MetadataStore { - pub fn new(repo_path: impl Into) -> Self { + pub fn new(repo_path: impl Into, author_name: &str, author_email: &str) -> Self { Self { repo_path: repo_path.into(), + author_name: author_name.to_string(), + author_email: author_email.to_string(), } } @@ -301,7 +309,7 @@ impl MetadataStore { let repo = Repository::discover(&self.repo_path) .map_err(|e| git_error(format!("failed to open repo: {e}")))?; let store = Store::new(repo); - let sig = Signature::now("arc", "arc@local") + let sig = Signature::now(&self.author_name, &self.author_email) .map_err(|e| git_error(format!("failed to create signature: {e}")))?; Ok((store, sig)) } @@ -518,7 +526,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, &[], "arc", "arc@local").unwrap(); let advanced_sha = head_sha(&wt).unwrap(); remove_worktree(dir.path(), &wt).unwrap(); @@ -590,7 +598,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), &[], "arc", "arc@local").unwrap(); assert_eq!(sha.len(), 40); assert!(sha.chars().all(|c| c.is_ascii_hexdigit())); @@ -629,7 +637,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, &[], "arc", "arc@local").unwrap(); assert_eq!(sha.len(), 40); // Verify Arc-Completed trailer present but no Arc-Meta @@ -673,7 +681,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, &[], "arc", "arc@local").unwrap(); assert_eq!(sha.len(), 40); remove_worktree(dir.path(), &wt_path).unwrap(); @@ -727,7 +735,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); init_repo(dir.path()); - let store = MetadataStore::new(dir.path()); + let store = MetadataStore::new(dir.path(), "arc", "arc@local"); let manifest = br#"{"run_id":"RUN1","workflow_name":"test","goal":"g","start_time":"2025-01-01T00:00:00Z","node_count":2,"edge_count":1}"#; let dot = b"digraph { start -> end }"; store.init_run("RUN1", manifest, dot).unwrap(); @@ -749,7 +757,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); init_repo(dir.path()); - let store = MetadataStore::new(dir.path()); + let store = MetadataStore::new(dir.path(), "arc", "arc@local"); store.init_run("RUN2", b"{}", b"digraph {}").unwrap(); let ctx = crate::context::Context::new(); @@ -784,7 +792,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); init_repo(dir.path()); - let store = MetadataStore::new(dir.path()); + let store = MetadataStore::new(dir.path(), "arc", "arc@local"); store.init_run("RUN3", b"{}", b"digraph {}").unwrap(); let ctx = crate::context::Context::new(); @@ -835,7 +843,7 @@ mod tests { let dir = tempfile::tempdir().unwrap(); init_repo(dir.path()); - let store = MetadataStore::new(dir.path()); + let store = MetadataStore::new(dir.path(), "arc", "arc@local"); store.init_run("RUN4", b"{}", b"digraph {}").unwrap(); let artifact_data = br#"{"large_output":"some data"}"#; @@ -958,7 +966,7 @@ mod tests { 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(); + checkpoint_commit(&wt_path, "run", "node", "ok", 1, None, &excludes, "arc", "arc@local").unwrap(); // Verify kept.txt was committed let output = Command::new("git") @@ -991,14 +999,14 @@ mod tests { // 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(); + checkpoint_commit(&wt_path, "run", "setup", "ok", 0, None, &[], "arc", "arc@local").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(); + checkpoint_commit(&wt_path, "run", "step", "ok", 1, None, &excludes, "arc", "arc@local").unwrap(); let output = Command::new("git") .args(["show", "--name-only", "--format=", "HEAD"]) diff --git a/crates/arc-workflows/src/handler/manager_loop.rs b/crates/arc-workflows/src/handler/manager_loop.rs index a7073f5f4..2bd144776 100644 --- a/crates/arc-workflows/src/handler/manager_loop.rs +++ b/crates/arc-workflows/src/handler/manager_loop.rs @@ -137,6 +137,7 @@ impl Handler for SubWorkflowHandler { let cancel_token = Arc::new(AtomicBool::new(false)); let child_cancel = Arc::clone(&cancel_token); + let git_state = services.git_state(); let child_config = RunConfig { logs_root: child_logs, cancel_token: Some(cancel_token), @@ -149,6 +150,8 @@ impl Handler for SubWorkflowHandler { labels: HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: git_state.as_ref().map(|gs| gs.git_author_name.clone()).unwrap_or_else(|| "arc".into()), + git_author_email: git_state.as_ref().map(|gs| gs.git_author_email.clone()).unwrap_or_else(|| "arc@local".into()), }; // 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 b99e6b062..84e0e0e66 100644 --- a/crates/arc-workflows/src/handler/parallel.rs +++ b/crates/arc-workflows/src/handler/parallel.rs @@ -244,6 +244,8 @@ impl Handler for ParallelHandler { 0, None, gs.checkpoint_exclude_globs.clone(), + gs.git_author_name.clone(), + gs.git_author_email.clone(), ) .await } @@ -256,6 +258,8 @@ impl Handler for ParallelHandler { 0, None, &gs.checkpoint_exclude_globs, + &gs.git_author_name, + &gs.git_author_email, ) .await } @@ -397,6 +401,8 @@ impl Handler for ParallelHandler { let sem = Arc::clone(&semaphore); let has_git = git_state.is_some(); let run_id = git_state.as_ref().map(|gs| gs.run_id.clone()); + let git_author_name = git_state.as_ref().map(|gs| gs.git_author_name.clone()).unwrap_or_else(|| "arc".into()); + let git_author_email = git_state.as_ref().map(|gs| gs.git_author_email.clone()).unwrap_or_else(|| "arc@local".into()); let handle = tokio::spawn(async move { let _permit = sem @@ -462,7 +468,7 @@ impl Handler for ParallelHandler { if add_result.as_ref().is_ok_and(|r| r.exit_code == 0) { let msg = format!("arc({rid}): {nid} ({status_str})"); let commit_cmd = format!( - "{git_r} -c user.name=arc -c user.email=arc@local commit --allow-empty -m '{msg}'" + "{git_r} -c user.name={git_author_name} -c user.email={git_author_email} commit --allow-empty -m '{msg}'" ); let _ = setup .sandbox diff --git a/crates/arc-workflows/tests/daytona_integration.rs b/crates/arc-workflows/tests/daytona_integration.rs index 051983e0c..7f5117efa 100644 --- a/crates/arc-workflows/tests/daytona_integration.rs +++ b/crates/arc-workflows/tests/daytona_integration.rs @@ -333,6 +333,8 @@ async fn daytona_pipeline_artifact_offload_and_sync() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -526,6 +528,8 @@ async fn daytona_git_checkpoint_remote_emits_events() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -708,6 +712,8 @@ async fn daytona_parallel_git_branching_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1035,6 +1041,8 @@ async fn daytona_git_checkpoint_with_shadow_branch() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1174,6 +1182,8 @@ async fn daytona_asset_collection() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1406,6 +1416,8 @@ async fn daytona_git_push_run_branch_to_origin() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine diff --git a/crates/arc-workflows/tests/integration.rs b/crates/arc-workflows/tests/integration.rs index 30cd39f00..c3dc9d739 100644 --- a/crates/arc-workflows/tests/integration.rs +++ b/crates/arc-workflows/tests/integration.rs @@ -201,6 +201,8 @@ async fn end_to_end_linear_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -340,6 +342,8 @@ async fn end_to_end_branching_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -463,6 +467,8 @@ async fn end_to_end_human_gate_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -574,6 +580,8 @@ async fn goal_gate_routes_to_retry_target_on_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -695,6 +703,8 @@ async fn goal_gate_routes_to_retry_target_when_present() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1003,6 +1013,8 @@ async fn retry_on_failure_then_succeed() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1078,6 +1090,8 @@ async fn pipeline_with_many_nodes() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1401,6 +1415,8 @@ async fn smoke_test_with_mock_codergen_backend() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1502,6 +1518,8 @@ async fn end_to_end_parallel_fan_out_fan_in() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1614,6 +1632,8 @@ async fn resume_from_checkpoint_completes_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -1712,6 +1732,8 @@ async fn resume_from_checkpoint_preserves_goal_gate_outcomes() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; // This should succeed because goal gate for gated_work is satisfied @@ -1755,6 +1777,8 @@ async fn graph_goal_in_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -1790,6 +1814,8 @@ async fn event_streaming_lifecycle() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -1869,6 +1895,8 @@ async fn context_flow_between_stages() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -1921,6 +1949,8 @@ async fn tool_handler_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -1992,6 +2022,8 @@ async fn auto_approve_interviewer_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2028,6 +2060,8 @@ async fn codergen_without_backend_simulated() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -2132,6 +2166,8 @@ async fn branching_loop_back_on_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2218,6 +2254,8 @@ async fn human_gate_loops_back() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2275,6 +2313,8 @@ async fn scenario_ship_a_feature() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2360,6 +2400,8 @@ async fn scenario_parallel_expert_review() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2439,6 +2481,8 @@ async fn scenario_node_retries_on_retry_status() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2500,6 +2544,8 @@ async fn scenario_loop_restart_resets_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2567,6 +2613,8 @@ async fn scenario_bug_triage_router() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2624,6 +2672,8 @@ async fn scenario_crash_recovery() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -2732,6 +2782,8 @@ async fn manager_loop_stop_condition_satisfied_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -2808,6 +2860,8 @@ async fn manager_loop_max_cycles_exceeded_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -2943,6 +2997,8 @@ async fn conditional_branching_success_fail_paths() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -2995,6 +3051,8 @@ async fn edge_selection_condition_match_wins_over_weight() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3041,6 +3099,8 @@ async fn edge_selection_weight_breaks_ties() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3079,6 +3139,8 @@ async fn edge_selection_lexical_tiebreak() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3136,6 +3198,8 @@ async fn context_updates_visible_across_nodes() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3179,6 +3243,8 @@ async fn stylesheet_applies_model_override() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -3234,6 +3300,8 @@ async fn custom_handler_registration_and_execution() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3304,6 +3372,8 @@ async fn integration_smoke_plan_implement_review_done() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success); @@ -3407,6 +3477,8 @@ async fn manager_loop_runs_child_engine_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -3541,6 +3613,8 @@ async fn manager_loop_context_flows_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -3614,6 +3688,8 @@ async fn manager_loop_child_dotfile_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); @@ -3727,6 +3803,8 @@ async fn graph_merge_e2e_through_engine() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -3877,6 +3955,8 @@ async fn fidelity_default_is_compact() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3933,6 +4013,8 @@ async fn fidelity_graph_default_applied() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -3985,6 +4067,8 @@ async fn fidelity_node_overrides_graph_default() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4043,6 +4127,8 @@ async fn fidelity_edge_overrides_node_and_graph() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4091,6 +4177,8 @@ async fn fidelity_full_produces_empty_preamble() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4149,6 +4237,8 @@ async fn fidelity_truncate_preamble_minimal() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4220,6 +4310,8 @@ async fn fidelity_summary_low_mode() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4286,6 +4378,8 @@ async fn fidelity_summary_medium_mode() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4352,6 +4446,8 @@ async fn fidelity_summary_high_mode() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4411,6 +4507,8 @@ async fn fidelity_full_sets_thread_id_in_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4481,6 +4579,8 @@ async fn fidelity_full_nodes_share_thread_id() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4560,6 +4660,8 @@ async fn fidelity_resume_degrades_full_to_summary_high() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -4655,6 +4757,8 @@ async fn fidelity_resume_degrade_only_affects_first_hop() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -4737,6 +4841,8 @@ async fn fidelity_resume_no_degrade_when_not_full() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -4778,6 +4884,8 @@ async fn fidelity_stored_in_checkpoint_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4863,6 +4971,8 @@ async fn fidelity_precedence_multi_node_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -4930,6 +5040,8 @@ async fn fidelity_compact_preamble_includes_completed_stages_and_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5005,6 +5117,8 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine_low .run(&graph_low, &config_low) @@ -5072,6 +5186,8 @@ async fn fidelity_summary_low_excludes_context_values_in_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine_med .run(&graph_med, &config_med) @@ -5142,6 +5258,8 @@ async fn fidelity_thread_id_fallback_to_previous_node_in_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5195,6 +5313,8 @@ async fn fidelity_thread_id_from_node_class_in_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5251,6 +5371,8 @@ async fn fidelity_edge_thread_id_override_in_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5308,6 +5430,8 @@ async fn fidelity_full_without_explicit_thread_id_uses_previous_node() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5375,6 +5499,8 @@ async fn fidelity_from_parsed_dot_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5422,6 +5548,8 @@ async fn fidelity_checkpoint_roundtrip_preserves_fidelity() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5491,6 +5619,8 @@ async fn fidelity_node_thread_id_overrides_edge_thread_id_in_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine.run(&graph, &config).await.expect("run"); @@ -5576,6 +5706,8 @@ async fn fidelity_resume_preserves_context_values_across_checkpoint() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine .run_from_checkpoint(&graph, &config, &checkpoint) @@ -5769,6 +5901,8 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = tokio::time::timeout( @@ -5883,6 +6017,8 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = tokio::time::timeout( @@ -6024,6 +6160,8 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = tokio::time::timeout( @@ -6131,6 +6269,8 @@ mod real_llm { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = tokio::time::timeout( @@ -6229,6 +6369,8 @@ async fn human_gate_freeform_only_routes_text() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -6362,6 +6504,8 @@ async fn human_gate_freeform_with_fixed_choice_match() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -6479,6 +6623,8 @@ async fn human_gate_freeform_fallback_on_unmatched_text() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -6610,6 +6756,8 @@ async fn human_gate_freeform_sets_allow_freeform_on_question() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -6721,6 +6869,8 @@ async fn human_gate_without_freeform_sets_allow_freeform_false() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -6980,6 +7130,8 @@ fn make_run_config(dir: &std::path::Path) -> RunConfig { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), } } @@ -8073,6 +8225,8 @@ async fn arc_e2e_with_real_llm() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -8201,6 +8355,8 @@ async fn run_fidelity_prompt_pipeline(fidelity: &str) -> String { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; engine @@ -8400,6 +8556,8 @@ async fn large_context_values_are_offloaded_to_artifact_store() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -8612,6 +8770,8 @@ async fn artifact_pointers_rewritten_for_remote_sandbox() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -8742,6 +8902,8 @@ async fn node_dir_uses_visit_count_on_revisit() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -9660,6 +9822,8 @@ async fn full_pipeline_with_cli_backend_node() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -9789,6 +9953,8 @@ async fn stylesheet_backend_property_routes_to_cli() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -10070,6 +10236,8 @@ async fn git_checkpoint_host_emits_events_and_diff_patch() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; // 5. Run pipeline @@ -10255,6 +10423,8 @@ async fn git_checkpoint_host_writes_shadow_branch() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; // 5. Run pipeline @@ -10449,6 +10619,8 @@ async fn parallel_git_branching_host_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; // 5. Run pipeline @@ -10711,6 +10883,8 @@ async fn git_checkpoint_host_skips_empty_diff_patch() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -11093,6 +11267,8 @@ async fn e2e_circuit_breaker_deterministic_self_loop() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11140,6 +11316,8 @@ async fn e2e_circuit_breaker_custom_limit() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11180,6 +11358,8 @@ async fn e2e_circuit_breaker_ignores_transient_failures() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11227,6 +11407,8 @@ async fn e2e_circuit_breaker_different_reasons_separate_counters() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11267,6 +11449,8 @@ async fn e2e_circuit_breaker_loop_restart() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11329,6 +11513,8 @@ async fn e2e_failure_signature_persisted_in_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.unwrap(); @@ -11393,6 +11579,8 @@ async fn e2e_failure_signature_hint_overrides_reason_in_context() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let _outcome = engine.run(&graph, &config).await.unwrap(); @@ -11449,6 +11637,8 @@ async fn e2e_signature_maps_persist_in_checkpoint() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.unwrap(); @@ -11575,6 +11765,8 @@ async fn e2e_circuit_breaker_emits_events_before_abort() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11642,6 +11834,8 @@ async fn e2e_circuit_breaker_does_not_fire_below_limit() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.unwrap(); @@ -11738,6 +11932,8 @@ async fn e2e_circuit_breaker_multi_stage_impl_verify_cycle() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11834,6 +12030,8 @@ async fn e2e_loop_restart_blocked_for_deterministic_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11874,6 +12072,8 @@ async fn e2e_loop_restart_blocked_for_structural_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11914,6 +12114,8 @@ async fn e2e_loop_restart_blocked_for_budget_exhausted_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11954,6 +12156,8 @@ async fn e2e_loop_restart_blocked_for_canceled_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -11991,6 +12195,8 @@ async fn e2e_loop_restart_blocked_for_compilation_loop_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -12032,6 +12238,8 @@ async fn e2e_loop_restart_allowed_for_transient_infra() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -12136,6 +12344,8 @@ async fn e2e_stall_watchdog_triggers_from_dot_parsed_pipeline() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let result = engine.run(&graph, &config).await; @@ -12192,6 +12402,8 @@ async fn e2e_stall_watchdog_kept_alive_by_handler_events() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -12238,6 +12450,8 @@ async fn e2e_stall_watchdog_disabled_with_zero_timeout() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -12303,6 +12517,8 @@ async fn e2e_stall_watchdog_with_explicit_timeout_override() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let start = std::time::Instant::now(); @@ -12433,6 +12649,8 @@ async fn asset_collection_local_sandbox_success() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -12541,6 +12759,8 @@ async fn asset_collection_local_sandbox_on_failure() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -12632,6 +12852,8 @@ async fn asset_collection_docker_sandbox() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine @@ -12701,6 +12923,8 @@ async fn wait_timer_e2e() { labels: std::collections::HashMap::new(), checkpoint_exclude_globs: Vec::new(), github_app: None, + git_author_name: "arc".into(), + git_author_email: "arc@local".into(), }; let outcome = engine.run(&graph, &config).await.expect("run"); assert_eq!(outcome.status, StageStatus::Success);