From c143d2c873a295df49ed073d547b9639f14366dc Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sun, 8 Mar 2026 11:56:41 -0400 Subject: [PATCH] Make goal optional in run config TOML so it can be provided via CLI --goal Precedence: CLI --goal > TOML goal > DOT graph attribute. Previously the TOML goal field was required but never applied to the graph. Co-Authored-By: Claude Opus 4.6 --- crates/arc-workflows/src/cli/run.rs | 47 +++++++++++++++------- crates/arc-workflows/src/cli/run_config.rs | 16 ++++++-- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/crates/arc-workflows/src/cli/run.rs b/crates/arc-workflows/src/cli/run.rs index ac20155ed..ca90943e9 100644 --- a/crates/arc-workflows/src/cli/run.rs +++ b/crates/arc-workflows/src/cli/run.rs @@ -36,10 +36,16 @@ use super::{ RunArgs, SandboxProvider, }; -/// Apply a CLI `--goal` override to the graph, if provided. -fn apply_goal_override(graph: &mut crate::graph::types::Graph, cli_goal: Option<&str>) { - if let Some(goal) = cli_goal { - debug!(goal = %goal, "CLI --goal overriding graph goal"); +/// Apply goal to the graph from TOML config or CLI flag. +/// Precedence: CLI `--goal` > TOML `goal` > DOT `graph [goal="..."]`. +fn apply_goal_override( + graph: &mut crate::graph::types::Graph, + cli_goal: Option<&str>, + toml_goal: Option<&str>, +) { + let goal = cli_goal.or(toml_goal); + if let Some(goal) = goal { + debug!(goal = %goal, "overriding graph goal"); graph.attrs.insert( "goal".to_string(), crate::graph::types::AttrValue::String(goal.to_string()), @@ -248,7 +254,8 @@ pub async fn run_command( dot_dir.to_path_buf(), ))); let (mut graph, diagnostics) = builder.prepare(&source)?; - apply_goal_override(&mut graph, args.goal.as_deref()); + let toml_goal = run_cfg.as_ref().and_then(|c| c.goal.as_deref()); + apply_goal_override(&mut graph, args.goal.as_deref(), toml_goal); // Inline @file references in the (possibly overridden) goal if let Some(crate::graph::types::AttrValue::String(goal)) = graph.attrs.get("goal") { @@ -1009,7 +1016,7 @@ async fn run_from_branch( }; let (mut graph, diagnostics) = crate::workflow::WorkflowBuilder::new().prepare(&source)?; - apply_goal_override(&mut graph, args.goal.as_deref()); + apply_goal_override(&mut graph, args.goal.as_deref(), None); eprintln!( "{} {} from branch {} ({})", @@ -1587,17 +1594,29 @@ mod tests { use super::*; #[test] - fn apply_goal_override_replaces_graph_goal() { + fn apply_goal_override_cli_wins_over_toml() { use crate::graph::types::{AttrValue, Graph}; let mut graph = Graph::new("test"); graph.attrs.insert( "goal".to_string(), AttrValue::String("original".to_string()), ); - apply_goal_override(&mut graph, Some("CLI goal")); + apply_goal_override(&mut graph, Some("CLI goal"), Some("TOML goal")); assert_eq!(graph.goal(), "CLI goal"); } + #[test] + fn apply_goal_override_toml_wins_over_dot() { + use crate::graph::types::{AttrValue, Graph}; + let mut graph = Graph::new("test"); + graph.attrs.insert( + "goal".to_string(), + AttrValue::String("original".to_string()), + ); + apply_goal_override(&mut graph, None, Some("TOML goal")); + assert_eq!(graph.goal(), "TOML goal"); + } + #[test] fn apply_goal_override_noop_when_none() { use crate::graph::types::{AttrValue, Graph}; @@ -1606,7 +1625,7 @@ mod tests { "goal".to_string(), AttrValue::String("original".to_string()), ); - apply_goal_override(&mut graph, None); + apply_goal_override(&mut graph, None, None); assert_eq!(graph.goal(), "original"); } @@ -1626,7 +1645,7 @@ mod tests { let defaults = RunDefaults::default(); let cfg = run_config::WorkflowRunConfig { version: 1, - goal: "test".to_string(), + goal: Some("test".to_string()), graph: "test.dot".to_string(), directory: None, llm: Some(run_config::LlmConfig { @@ -1667,7 +1686,7 @@ mod tests { let defaults = RunDefaults::default(); let cfg = run_config::WorkflowRunConfig { version: 1, - goal: "test".to_string(), + goal: Some("test".to_string()), graph: "test.dot".to_string(), directory: None, llm: Some(run_config::LlmConfig { @@ -1743,7 +1762,7 @@ mod tests { }; let cfg = run_config::WorkflowRunConfig { version: 1, - goal: "test".to_string(), + goal: Some("test".to_string()), graph: "test.dot".to_string(), directory: None, llm: Some(run_config::LlmConfig { @@ -1766,7 +1785,7 @@ mod tests { fn resolve_preserve_sandbox_cli_wins() { let cfg = run_config::WorkflowRunConfig { version: 1, - goal: "test".into(), + goal: Some("test".to_string()), graph: "w.dot".into(), directory: None, llm: None, @@ -1790,7 +1809,7 @@ mod tests { fn resolve_preserve_sandbox_toml_wins_over_defaults() { let cfg = run_config::WorkflowRunConfig { version: 1, - goal: "test".into(), + goal: Some("test".to_string()), graph: "w.dot".into(), directory: None, llm: None, diff --git a/crates/arc-workflows/src/cli/run_config.rs b/crates/arc-workflows/src/cli/run_config.rs index 902c98dee..e0c3eb9cf 100644 --- a/crates/arc-workflows/src/cli/run_config.rs +++ b/crates/arc-workflows/src/cli/run_config.rs @@ -20,7 +20,7 @@ pub struct CheckpointConfig { #[serde(deny_unknown_fields)] pub struct WorkflowRunConfig { pub version: u32, - pub goal: String, + pub goal: Option, pub graph: String, pub directory: Option, pub llm: Option, @@ -514,13 +514,23 @@ graph = "workflow.dot" "#; let config = parse_run_config(toml).unwrap(); assert_eq!(config.version, 1); - assert_eq!(config.goal, "Run tests"); + assert_eq!(config.goal.as_deref(), Some("Run tests")); assert_eq!(config.graph, "workflow.dot"); assert!(config.directory.is_none()); assert!(config.llm.is_none()); assert!(config.setup.is_none()); } + #[test] + fn parse_toml_without_goal() { + let toml = r#" +version = 1 +graph = "workflow.dot" +"#; + let config = parse_run_config(toml).unwrap(); + assert!(config.goal.is_none()); + } + #[test] fn parse_full_toml() { let toml = r#" @@ -538,7 +548,7 @@ commands = ["pip install -r requirements.txt", "npm install"] timeout_ms = 60000 "#; let config = parse_run_config(toml).unwrap(); - assert_eq!(config.goal, "Full workflow"); + assert_eq!(config.goal.as_deref(), Some("Full workflow")); assert_eq!(config.directory.as_deref(), Some("/tmp/repo")); let llm = config.llm.unwrap();