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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-03-08 11:56:41 -04:00
parent 1cd3be878a
commit c143d2c873
2 changed files with 46 additions and 17 deletions

View file

@ -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,

View file

@ -20,7 +20,7 @@ pub struct CheckpointConfig {
#[serde(deny_unknown_fields)]
pub struct WorkflowRunConfig {
pub version: u32,
pub goal: String,
pub goal: Option<String>,
pub graph: String,
pub directory: Option<String>,
pub llm: Option<LlmConfig>,
@ -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();