([]);
@@ -133,6 +197,7 @@ export function AgentPanel({ stories }: AgentPanelProps) {
log: [],
sessionId: a.session_id,
worktreePath: a.worktree_path,
+ baseBranch: a.base_branch,
};
if (a.status === "running" || a.status === "pending") {
subscribeToAgent(a.story_id, a.agent_name);
@@ -166,6 +231,7 @@ export function AgentPanel({ stories }: AgentPanelProps) {
log: [],
sessionId: null,
worktreePath: null,
+ baseBranch: null,
};
switch (event.type) {
@@ -241,6 +307,7 @@ export function AgentPanel({ stories }: AgentPanelProps) {
log: [],
sessionId: info.session_id,
worktreePath: info.worktree_path,
+ baseBranch: info.base_branch,
},
}));
setExpandedKey(key);
@@ -609,6 +676,12 @@ export function AgentPanel({ stories }: AgentPanelProps) {
Worktree: {a.worktreePath}
)}
+ {a.worktreePath && (
+
+ )}
,
pub worktree_path: Option,
+ pub base_branch: Option,
}
struct StoryAgent {
@@ -182,7 +183,7 @@ impl AgentPool {
// Spawn the agent process
let wt_path_str = wt_info.path.to_string_lossy().to_string();
let (command, args, prompt) =
- config.render_agent_args(&wt_path_str, story_id, Some(&resolved_name))?;
+ config.render_agent_args(&wt_path_str, story_id, Some(&resolved_name), Some(&wt_info.base_branch))?;
let sid = story_id.to_string();
let aname = resolved_name.clone();
@@ -247,6 +248,7 @@ impl AgentPool {
status: AgentStatus::Running,
session_id: None,
worktree_path: Some(wt_path_str),
+ base_branch: Some(wt_info.base_branch.clone()),
})
}
@@ -321,6 +323,10 @@ impl AgentPool {
.worktree_info
.as_ref()
.map(|wt| wt.path.to_string_lossy().to_string()),
+ base_branch: agent
+ .worktree_info
+ .as_ref()
+ .map(|wt| wt.base_branch.clone()),
}
})
.collect())
diff --git a/server/src/config.rs b/server/src/config.rs
index c378ff8..c0cb7cf 100644
--- a/server/src/config.rs
+++ b/server/src/config.rs
@@ -175,6 +175,7 @@ impl ProjectConfig {
worktree_path: &str,
story_id: &str,
agent_name: Option<&str>,
+ base_branch: Option<&str>,
) -> Result<(String, Vec, String), String> {
let agent = match agent_name {
Some(name) => self
@@ -185,9 +186,11 @@ impl ProjectConfig {
.ok_or_else(|| "No agents configured".to_string())?,
};
+ let bb = base_branch.unwrap_or("master");
let render = |s: &str| {
s.replace("{{worktree_path}}", worktree_path)
.replace("{{story_id}}", story_id)
+ .replace("{{base_branch}}", bb)
};
let command = render(&agent.command);
@@ -378,7 +381,7 @@ max_turns = 0
fn render_agent_args_default() {
let config = ProjectConfig::default();
let (cmd, args, prompt) = config
- .render_agent_args("/tmp/wt", "42_foo", None)
+ .render_agent_args("/tmp/wt", "42_foo", None, None)
.unwrap();
assert_eq!(cmd, "claude");
assert!(args.is_empty());
@@ -404,7 +407,7 @@ max_turns = 30
let config = ProjectConfig::parse(toml_str).unwrap();
let (cmd, args, prompt) = config
- .render_agent_args("/tmp/wt", "42_foo", Some("supervisor"))
+ .render_agent_args("/tmp/wt", "42_foo", Some("supervisor"), Some("master"))
.unwrap();
assert_eq!(cmd, "claude");
assert!(args.contains(&"--model".to_string()));
@@ -422,7 +425,7 @@ max_turns = 30
// Render for coder
let (_, coder_args, _) = config
- .render_agent_args("/tmp/wt", "42_foo", Some("coder"))
+ .render_agent_args("/tmp/wt", "42_foo", Some("coder"), Some("master"))
.unwrap();
assert!(coder_args.contains(&"sonnet".to_string()));
assert!(coder_args.contains(&"30".to_string()));
@@ -433,7 +436,7 @@ max_turns = 30
#[test]
fn render_agent_args_not_found() {
let config = ProjectConfig::default();
- let result = config.render_agent_args("/tmp/wt", "42_foo", Some("nonexistent"));
+ let result = config.render_agent_args("/tmp/wt", "42_foo", Some("nonexistent"), None);
assert!(result.is_err());
assert!(result.unwrap_err().contains("No agent named 'nonexistent'"));
}
diff --git a/server/src/worktree.rs b/server/src/worktree.rs
index 77e7f4f..21edd2a 100644
--- a/server/src/worktree.rs
+++ b/server/src/worktree.rs
@@ -7,6 +7,7 @@ use std::process::Command;
pub struct WorktreeInfo {
pub path: PathBuf,
pub branch: String,
+ pub base_branch: String,
}
/// Worktree path as a sibling of the project root: `{project_root}-story-{id}`.
@@ -24,6 +25,23 @@ fn branch_name(story_id: &str) -> String {
format!("feature/story-{story_id}")
}
+/// Detect the current branch of the project root (the base branch worktrees fork from).
+fn detect_base_branch(project_root: &Path) -> String {
+ Command::new("git")
+ .args(["rev-parse", "--abbrev-ref", "HEAD"])
+ .current_dir(project_root)
+ .output()
+ .ok()
+ .and_then(|o| {
+ if o.status.success() {
+ Some(String::from_utf8_lossy(&o.stdout).trim().to_string())
+ } else {
+ None
+ }
+ })
+ .unwrap_or_else(|| "master".to_string())
+}
+
/// Create a git worktree for the given story.
///
/// - Creates the worktree at `{project_root}-story-{story_id}` (sibling directory)
@@ -37,6 +55,7 @@ pub async fn create_worktree(
) -> Result {
let wt_path = worktree_path(project_root, story_id);
let branch = branch_name(story_id);
+ let base_branch = detect_base_branch(project_root);
let root = project_root.to_path_buf();
// Already exists — reuse
@@ -45,6 +64,7 @@ pub async fn create_worktree(
return Ok(WorktreeInfo {
path: wt_path,
branch,
+ base_branch,
});
}
@@ -60,6 +80,7 @@ pub async fn create_worktree(
Ok(WorktreeInfo {
path: wt_path,
branch,
+ base_branch,
})
}