storkit: merge 387_story_configurable_base_branch_name_in_project_toml
This commit is contained in:
@@ -13,6 +13,11 @@ max_coders = 3
|
|||||||
# Set to 0 to disable retry limits.
|
# Set to 0 to disable retry limits.
|
||||||
max_retries = 2
|
max_retries = 2
|
||||||
|
|
||||||
|
# Base branch name for this project. Worktree creation, merges, and agent prompts
|
||||||
|
# use this value for {{base_branch}}. When not set, falls back to auto-detection
|
||||||
|
# (reads current HEAD branch).
|
||||||
|
base_branch = "master"
|
||||||
|
|
||||||
[[component]]
|
[[component]]
|
||||||
name = "frontend"
|
name = "frontend"
|
||||||
path = "frontend"
|
path = "frontend"
|
||||||
|
|||||||
43
.storkit/project.toml.example
Normal file
43
.storkit/project.toml.example
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# Example project.toml — copy to .storkit/project.toml and customise.
|
||||||
|
# This file is checked in; project.toml itself is gitignored (it may contain
|
||||||
|
# instance-specific settings).
|
||||||
|
|
||||||
|
# Project-wide default QA mode: "server", "agent", or "human".
|
||||||
|
# Per-story `qa` front matter overrides this setting.
|
||||||
|
default_qa = "server"
|
||||||
|
|
||||||
|
# Default model for coder agents. Only agents with this model are auto-assigned.
|
||||||
|
# Opus coders are reserved for explicit per-story `agent:` front matter requests.
|
||||||
|
default_coder_model = "sonnet"
|
||||||
|
|
||||||
|
# Maximum concurrent coder agents. Stories wait in 2_current/ when all slots are full.
|
||||||
|
max_coders = 3
|
||||||
|
|
||||||
|
# Maximum retries per story per pipeline stage before marking as blocked.
|
||||||
|
# Set to 0 to disable retry limits.
|
||||||
|
max_retries = 2
|
||||||
|
|
||||||
|
# Base branch name for this project. Worktree creation, merges, and agent prompts
|
||||||
|
# use this value for {{base_branch}}. When not set, falls back to auto-detection
|
||||||
|
# (reads current HEAD branch).
|
||||||
|
base_branch = "main"
|
||||||
|
|
||||||
|
[[component]]
|
||||||
|
name = "server"
|
||||||
|
path = "."
|
||||||
|
setup = ["cargo build"]
|
||||||
|
teardown = []
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder-1"
|
||||||
|
role = "Full-stack engineer"
|
||||||
|
stage = "coder"
|
||||||
|
model = "sonnet"
|
||||||
|
max_turns = 50
|
||||||
|
max_budget_usd = 5.00
|
||||||
|
prompt = """
|
||||||
|
You are working in a git worktree on story {{story_id}}.
|
||||||
|
Read CLAUDE.md first, then .storkit/README.md to understand the dev process.
|
||||||
|
Run: cd "{{worktree_path}}" && git difftool {{base_branch}}...HEAD
|
||||||
|
Commit all your work before your process exits.
|
||||||
|
"""
|
||||||
@@ -30,6 +30,12 @@ pub struct ProjectConfig {
|
|||||||
/// Default: 2. Set to 0 to disable retry limits.
|
/// Default: 2. Set to 0 to disable retry limits.
|
||||||
#[serde(default = "default_max_retries")]
|
#[serde(default = "default_max_retries")]
|
||||||
pub max_retries: u32,
|
pub max_retries: u32,
|
||||||
|
/// Optional base branch name (e.g. "main", "master", "develop").
|
||||||
|
/// When set, overrides the auto-detection logic (`detect_base_branch`) for all
|
||||||
|
/// worktree creation, merge operations, and agent prompt `{{base_branch}}` substitution.
|
||||||
|
/// When not set, the system falls back to `detect_base_branch` (reads current HEAD).
|
||||||
|
#[serde(default)]
|
||||||
|
pub base_branch: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Configuration for the filesystem watcher's sweep behaviour.
|
/// Configuration for the filesystem watcher's sweep behaviour.
|
||||||
@@ -164,6 +170,8 @@ struct LegacyProjectConfig {
|
|||||||
max_coders: Option<usize>,
|
max_coders: Option<usize>,
|
||||||
#[serde(default = "default_max_retries")]
|
#[serde(default = "default_max_retries")]
|
||||||
max_retries: u32,
|
max_retries: u32,
|
||||||
|
#[serde(default)]
|
||||||
|
base_branch: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ProjectConfig {
|
impl Default for ProjectConfig {
|
||||||
@@ -190,6 +198,7 @@ impl Default for ProjectConfig {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: default_max_retries(),
|
max_retries: default_max_retries(),
|
||||||
|
base_branch: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,6 +244,7 @@ impl ProjectConfig {
|
|||||||
default_coder_model: legacy.default_coder_model,
|
default_coder_model: legacy.default_coder_model,
|
||||||
max_coders: legacy.max_coders,
|
max_coders: legacy.max_coders,
|
||||||
max_retries: legacy.max_retries,
|
max_retries: legacy.max_retries,
|
||||||
|
base_branch: legacy.base_branch,
|
||||||
};
|
};
|
||||||
validate_agents(&config.agent)?;
|
validate_agents(&config.agent)?;
|
||||||
return Ok(config);
|
return Ok(config);
|
||||||
@@ -259,6 +269,7 @@ impl ProjectConfig {
|
|||||||
default_coder_model: legacy.default_coder_model,
|
default_coder_model: legacy.default_coder_model,
|
||||||
max_coders: legacy.max_coders,
|
max_coders: legacy.max_coders,
|
||||||
max_retries: legacy.max_retries,
|
max_retries: legacy.max_retries,
|
||||||
|
base_branch: legacy.base_branch,
|
||||||
};
|
};
|
||||||
validate_agents(&config.agent)?;
|
validate_agents(&config.agent)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
@@ -271,6 +282,7 @@ impl ProjectConfig {
|
|||||||
default_coder_model: legacy.default_coder_model,
|
default_coder_model: legacy.default_coder_model,
|
||||||
max_coders: legacy.max_coders,
|
max_coders: legacy.max_coders,
|
||||||
max_retries: legacy.max_retries,
|
max_retries: legacy.max_retries,
|
||||||
|
base_branch: legacy.base_branch,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,7 +324,9 @@ impl ProjectConfig {
|
|||||||
.ok_or_else(|| "No agents configured".to_string())?,
|
.ok_or_else(|| "No agents configured".to_string())?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let bb = base_branch.unwrap_or("master");
|
let bb = base_branch
|
||||||
|
.or(self.base_branch.as_deref())
|
||||||
|
.unwrap_or("master");
|
||||||
let aname = agent.name.as_str();
|
let aname = agent.name.as_str();
|
||||||
let render = |s: &str| {
|
let render = |s: &str| {
|
||||||
s.replace("{{worktree_path}}", worktree_path)
|
s.replace("{{worktree_path}}", worktree_path)
|
||||||
@@ -858,6 +872,80 @@ runtime = "openai"
|
|||||||
assert!(err.contains("unknown runtime 'openai'"));
|
assert!(err.contains("unknown runtime 'openai'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── base_branch config ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base_branch_defaults_to_none() {
|
||||||
|
let toml_str = r#"
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
assert_eq!(config.base_branch, None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn base_branch_parsed_when_set() {
|
||||||
|
let toml_str = r#"
|
||||||
|
base_branch = "main"
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
assert_eq!(config.base_branch, Some("main".to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_agent_args_uses_config_base_branch_when_caller_passes_none() {
|
||||||
|
let toml_str = r#"
|
||||||
|
base_branch = "develop"
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
prompt = "git difftool {{base_branch}}...HEAD"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
let (_, _, prompt) = config
|
||||||
|
.render_agent_args("/tmp/wt", "42_foo", None, None)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
prompt.contains("develop"),
|
||||||
|
"Expected 'develop' in prompt, got: {prompt}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_agent_args_caller_base_branch_takes_precedence_over_config() {
|
||||||
|
let toml_str = r#"
|
||||||
|
base_branch = "develop"
|
||||||
|
|
||||||
|
[[agent]]
|
||||||
|
name = "coder"
|
||||||
|
prompt = "git difftool {{base_branch}}...HEAD"
|
||||||
|
"#;
|
||||||
|
let config = ProjectConfig::parse(toml_str).unwrap();
|
||||||
|
let (_, _, prompt) = config
|
||||||
|
.render_agent_args("/tmp/wt", "42_foo", None, Some("feature-x"))
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
prompt.contains("feature-x"),
|
||||||
|
"Caller-supplied base_branch should win, got: {prompt}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_toml_has_base_branch_master() {
|
||||||
|
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let project_root = manifest_dir.parent().unwrap();
|
||||||
|
let config = ProjectConfig::load(project_root).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
config.base_branch,
|
||||||
|
Some("master".to_string()),
|
||||||
|
"project.toml must have base_branch = \"master\""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn project_toml_has_three_sonnet_coders() {
|
fn project_toml_has_three_sonnet_coders() {
|
||||||
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
let manifest_dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ pub async fn create_worktree(
|
|||||||
) -> Result<WorktreeInfo, String> {
|
) -> Result<WorktreeInfo, String> {
|
||||||
let wt_path = worktree_path(project_root, story_id);
|
let wt_path = worktree_path(project_root, story_id);
|
||||||
let branch = branch_name(story_id);
|
let branch = branch_name(story_id);
|
||||||
let base_branch = detect_base_branch(project_root);
|
let base_branch = config
|
||||||
|
.base_branch
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| detect_base_branch(project_root));
|
||||||
let root = project_root.to_path_buf();
|
let root = project_root.to_path_buf();
|
||||||
|
|
||||||
// Already exists — reuse (ensure sparse checkout is configured)
|
// Already exists — reuse (ensure sparse checkout is configured)
|
||||||
@@ -199,7 +202,10 @@ pub async fn remove_worktree_by_story_id(
|
|||||||
return Err(format!("Worktree not found for story: {story_id}"));
|
return Err(format!("Worktree not found for story: {story_id}"));
|
||||||
}
|
}
|
||||||
let branch = branch_name(story_id);
|
let branch = branch_name(story_id);
|
||||||
let base_branch = detect_base_branch(project_root);
|
let base_branch = config
|
||||||
|
.base_branch
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| detect_base_branch(project_root));
|
||||||
let info = WorktreeInfo {
|
let info = WorktreeInfo {
|
||||||
path,
|
path,
|
||||||
branch,
|
branch,
|
||||||
@@ -519,6 +525,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// Should complete without panic
|
// Should complete without panic
|
||||||
run_setup_commands(tmp.path(), &config).await;
|
run_setup_commands(tmp.path(), &config).await;
|
||||||
@@ -540,6 +547,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// Should complete without panic
|
// Should complete without panic
|
||||||
run_setup_commands(tmp.path(), &config).await;
|
run_setup_commands(tmp.path(), &config).await;
|
||||||
@@ -561,6 +569,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// Setup command failures are non-fatal — should not panic or propagate
|
// Setup command failures are non-fatal — should not panic or propagate
|
||||||
run_setup_commands(tmp.path(), &config).await;
|
run_setup_commands(tmp.path(), &config).await;
|
||||||
@@ -582,6 +591,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// Teardown failures are best-effort — should not propagate
|
// Teardown failures are best-effort — should not propagate
|
||||||
assert!(run_teardown_commands(tmp.path(), &config).await.is_ok());
|
assert!(run_teardown_commands(tmp.path(), &config).await.is_ok());
|
||||||
@@ -602,6 +612,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
let info = create_worktree(&project_root, "42_fresh_test", &config, 3001)
|
let info = create_worktree(&project_root, "42_fresh_test", &config, 3001)
|
||||||
.await
|
.await
|
||||||
@@ -629,6 +640,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// First creation
|
// First creation
|
||||||
let _info1 = create_worktree(&project_root, "43_reuse_test", &config, 3001)
|
let _info1 = create_worktree(&project_root, "43_reuse_test", &config, 3001)
|
||||||
@@ -697,6 +709,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let result = remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &config).await;
|
let result = remove_worktree_by_story_id(tmp.path(), "99_nonexistent", &config).await;
|
||||||
@@ -723,6 +736,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
create_worktree(&project_root, "88_remove_by_id", &config, 3001)
|
create_worktree(&project_root, "88_remove_by_id", &config, 3001)
|
||||||
.await
|
.await
|
||||||
@@ -796,6 +810,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// Even though setup commands fail, create_worktree must succeed
|
// Even though setup commands fail, create_worktree must succeed
|
||||||
// so the agent can start and fix the problem itself.
|
// so the agent can start and fix the problem itself.
|
||||||
@@ -825,6 +840,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// First creation — no setup commands, should succeed
|
// First creation — no setup commands, should succeed
|
||||||
create_worktree(&project_root, "173_reuse_fail", &empty_config, 3001)
|
create_worktree(&project_root, "173_reuse_fail", &empty_config, 3001)
|
||||||
@@ -844,6 +860,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
// Second call — worktree exists, setup commands fail, must still succeed
|
// Second call — worktree exists, setup commands fail, must still succeed
|
||||||
let result = create_worktree(&project_root, "173_reuse_fail", &failing_config, 3002).await;
|
let result = create_worktree(&project_root, "173_reuse_fail", &failing_config, 3002).await;
|
||||||
@@ -869,6 +886,7 @@ mod tests {
|
|||||||
default_coder_model: None,
|
default_coder_model: None,
|
||||||
max_coders: None,
|
max_coders: None,
|
||||||
max_retries: 2,
|
max_retries: 2,
|
||||||
|
base_branch: None,
|
||||||
};
|
};
|
||||||
let info = create_worktree(&project_root, "77_remove_async", &config, 3001)
|
let info = create_worktree(&project_root, "77_remove_async", &config, 3001)
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user