diff --git a/auto-claude-ui/src/main/agent/agent-manager.ts b/auto-claude-ui/src/main/agent/agent-manager.ts index 59634db8..514edb9b 100644 --- a/auto-claude-ui/src/main/agent/agent-manager.ts +++ b/auto-claude-ui/src/main/agent/agent-manager.ts @@ -125,6 +125,11 @@ export class AgentManager extends EventEmitter { // Force: When user starts a task from the UI, that IS their approval args.push('--force'); + // Pass base branch if specified (ensures worktrees are created from the correct branch) + if (options.baseBranch) { + args.push('--base-branch', options.baseBranch); + } + // Note: --parallel was removed from run.py CLI - parallel execution is handled internally by the agent // The options.parallel and options.workers are kept for future use or logging purposes diff --git a/auto-claude-ui/src/main/agent/types.ts b/auto-claude-ui/src/main/agent/types.ts index 527d484d..c51cf4c8 100644 --- a/auto-claude-ui/src/main/agent/types.ts +++ b/auto-claude-ui/src/main/agent/types.ts @@ -40,6 +40,7 @@ export interface IdeationConfig { export interface TaskExecutionOptions { parallel?: boolean; workers?: number; + baseBranch?: string; } export interface SpecCreationMetadata { diff --git a/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts index 22806980..b1b63efc 100644 --- a/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/project-handlers.ts @@ -1,6 +1,7 @@ import { ipcMain, app } from 'electron'; import { existsSync, readFileSync } from 'fs'; import path from 'path'; +import { execSync } from 'child_process'; import { IPC_CHANNELS } from '../../shared/constants'; import type { Project, @@ -22,6 +23,79 @@ import { insightsService } from '../insights-service'; import { titleGenerator } from '../title-generator'; import type { BrowserWindow } from 'electron'; +// ============================================ +// Git Helper Functions +// ============================================ + +/** + * Get list of git branches for a directory + */ +function getGitBranches(projectPath: string): string[] { + try { + const result = execSync('git branch --list --format="%(refname:short)"', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + return result.trim().split('\n').filter(b => b.trim()); + } catch { + return []; + } +} + +/** + * Get the current git branch for a directory + */ +function getCurrentGitBranch(projectPath: string): string | null { + try { + const result = execSync('git rev-parse --abbrev-ref HEAD', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + return result.trim() || null; + } catch { + return null; + } +} + +/** + * Detect the main branch for a git repository + * Checks for common main branch names in order of preference + */ +function detectMainBranch(projectPath: string): string | null { + const branches = getGitBranches(projectPath); + if (branches.length === 0) return null; + + // Check for common main branch names in order of preference + const mainBranchCandidates = ['main', 'master', 'develop', 'dev', 'trunk']; + for (const candidate of mainBranchCandidates) { + if (branches.includes(candidate)) { + return candidate; + } + } + + // If none of the common names found, check for origin/HEAD reference + try { + const result = execSync('git symbolic-ref refs/remotes/origin/HEAD', { + cwd: projectPath, + encoding: 'utf-8', + stdio: ['pipe', 'pipe', 'pipe'] + }); + const ref = result.trim(); + // Extract branch name from refs/remotes/origin/main + const match = ref.match(/refs\/remotes\/origin\/(.+)/); + if (match && branches.includes(match[1])) { + return match[1]; + } + } catch { + // origin/HEAD not set, continue with fallback + } + + // Fallback: return the first branch (usually the current one) + return branches[0] || null; +} + const settingsPath = path.join(app.getPath('userData'), 'settings.json'); /** @@ -330,4 +404,65 @@ export function registerProjectHandlers( } } ); + + // ============================================ + // Git Operations + // ============================================ + + // Get all branches for a project + ipcMain.handle( + IPC_CHANNELS.GIT_GET_BRANCHES, + async (_, projectPath: string): Promise> => { + try { + if (!existsSync(projectPath)) { + return { success: false, error: 'Directory does not exist' }; + } + const branches = getGitBranches(projectPath); + return { success: true, data: branches }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + // Get current branch for a project + ipcMain.handle( + IPC_CHANNELS.GIT_GET_CURRENT_BRANCH, + async (_, projectPath: string): Promise> => { + try { + if (!existsSync(projectPath)) { + return { success: false, error: 'Directory does not exist' }; + } + const branch = getCurrentGitBranch(projectPath); + return { success: true, data: branch }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); + + // Auto-detect main branch for a project + ipcMain.handle( + IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, + async (_, projectPath: string): Promise> => { + try { + if (!existsSync(projectPath)) { + return { success: false, error: 'Directory does not exist' }; + } + const mainBranch = detectMainBranch(projectPath); + return { success: true, data: mainBranch }; + } catch (error) { + return { + success: false, + error: error instanceof Error ? error.message : 'Unknown error' + }; + } + } + ); } diff --git a/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts b/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts index a2c39f16..7d70c2a0 100644 --- a/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts +++ b/auto-claude-ui/src/main/ipc-handlers/task/execution-handlers.ts @@ -62,6 +62,9 @@ export function registerTaskExecutionHandlers( console.log('[TASK_START] hasSpec:', hasSpec, 'needsSpecCreation:', needsSpecCreation, 'needsImplementation:', needsImplementation); + // Get base branch from project settings for worktree creation + const baseBranch = project.settings?.mainBranch; + if (needsSpecCreation) { // No spec file - need to run spec_runner.py to create the spec const taskDescription = task.description || task.title; @@ -89,7 +92,8 @@ export function registerTaskExecutionHandlers( task.specId, { parallel: false, // Sequential for planning phase - workers: 1 + workers: 1, + baseBranch } ); } else { @@ -103,7 +107,8 @@ export function registerTaskExecutionHandlers( task.specId, { parallel: false, - workers: 1 + workers: 1, + baseBranch } ); } diff --git a/auto-claude-ui/src/preload/api/project-api.ts b/auto-claude-ui/src/preload/api/project-api.ts index 70748e35..6b55c47c 100644 --- a/auto-claude-ui/src/preload/api/project-api.ts +++ b/auto-claude-ui/src/preload/api/project-api.ts @@ -67,6 +67,11 @@ export interface ProjectAPI { falkorDbUri: string, openAiApiKey: string ) => Promise>; + + // Git Operations + getGitBranches: (projectPath: string) => Promise>; + getCurrentGitBranch: (projectPath: string) => Promise>; + detectMainBranch: (projectPath: string) => Promise>; } export const createProjectAPI = (): ProjectAPI => ({ @@ -165,5 +170,15 @@ export const createProjectAPI = (): ProjectAPI => ({ falkorDbUri: string, openAiApiKey: string ): Promise> => - ipcRenderer.invoke(IPC_CHANNELS.GRAPHITI_TEST_CONNECTION, falkorDbUri, openAiApiKey) + ipcRenderer.invoke(IPC_CHANNELS.GRAPHITI_TEST_CONNECTION, falkorDbUri, openAiApiKey), + + // Git Operations + getGitBranches: (projectPath: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_BRANCHES, projectPath), + + getCurrentGitBranch: (projectPath: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.GIT_GET_CURRENT_BRANCH, projectPath), + + detectMainBranch: (projectPath: string): Promise> => + ipcRenderer.invoke(IPC_CHANNELS.GIT_DETECT_MAIN_BRANCH, projectPath) }); diff --git a/auto-claude-ui/src/renderer/components/AddProjectModal.tsx b/auto-claude-ui/src/renderer/components/AddProjectModal.tsx index 421998e3..1ffeecc8 100644 --- a/auto-claude-ui/src/renderer/components/AddProjectModal.tsx +++ b/auto-claude-ui/src/renderer/components/AddProjectModal.tsx @@ -63,6 +63,17 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje if (path) { const project = await addProject(path); if (project) { + // Auto-detect and save the main branch for the project + try { + const mainBranchResult = await window.electronAPI.detectMainBranch(path); + if (mainBranchResult.success && mainBranchResult.data) { + await window.electronAPI.updateProjectSettings(project.id, { + mainBranch: mainBranchResult.data + }); + } + } catch { + // Non-fatal - main branch can be set later in settings + } onProjectAdded?.(project, !project.autoBuildPath); onOpenChange(false); } @@ -112,6 +123,20 @@ export function AddProjectModal({ open, onOpenChange, onProjectAdded }: AddProje // Add the project to our store const project = await addProject(result.data.path); if (project) { + // For new projects with git init, set main branch + // Git init creates 'main' branch by default on modern git + if (initGit) { + try { + const mainBranchResult = await window.electronAPI.detectMainBranch(result.data.path); + if (mainBranchResult.success && mainBranchResult.data) { + await window.electronAPI.updateProjectSettings(project.id, { + mainBranch: mainBranchResult.data + }); + } + } catch { + // Non-fatal - main branch can be set later in settings + } + } onProjectAdded?.(project, true); // New projects always need init onOpenChange(false); } diff --git a/auto-claude-ui/src/renderer/components/project-settings/IntegrationSettings.tsx b/auto-claude-ui/src/renderer/components/project-settings/IntegrationSettings.tsx index a3ef48f7..e7cc2dc4 100644 --- a/auto-claude-ui/src/renderer/components/project-settings/IntegrationSettings.tsx +++ b/auto-claude-ui/src/renderer/components/project-settings/IntegrationSettings.tsx @@ -1,3 +1,4 @@ +import { useState, useEffect } from 'react'; import { Zap, Eye, @@ -10,19 +11,32 @@ import { Import, Radio, Github, - RefreshCw + RefreshCw, + GitBranch } from 'lucide-react'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Switch } from '../ui/switch'; import { Separator } from '../ui/separator'; -import type { ProjectEnvConfig, LinearSyncStatus, GitHubSyncStatus } from '../../../shared/types'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '../ui/select'; +import type { ProjectEnvConfig, LinearSyncStatus, GitHubSyncStatus, Project, ProjectSettings as ProjectSettingsType } from '../../../shared/types'; interface IntegrationSettingsProps { envConfig: ProjectEnvConfig | null; updateEnvConfig: (updates: Partial) => void; + // Project settings for main branch + project: Project; + settings: ProjectSettingsType; + setSettings: React.Dispatch>; + // Linear state showLinearKey: boolean; setShowLinearKey: React.Dispatch>; @@ -44,6 +58,9 @@ interface IntegrationSettingsProps { export function IntegrationSettings({ envConfig, updateEnvConfig, + project, + settings, + setSettings, showLinearKey, setShowLinearKey, linearConnectionStatus, @@ -58,6 +75,38 @@ export function IntegrationSettings({ githubExpanded, onGitHubToggle }: IntegrationSettingsProps) { + // Branch selection state + const [branches, setBranches] = useState([]); + const [isLoadingBranches, setIsLoadingBranches] = useState(false); + + // Load branches when GitHub section expands + useEffect(() => { + if (githubExpanded && project.path) { + loadBranches(); + } + }, [githubExpanded, project.path]); + + const loadBranches = async () => { + setIsLoadingBranches(true); + try { + const result = await window.electronAPI.getGitBranches(project.path); + if (result.success && result.data) { + setBranches(result.data); + // Auto-detect main branch if not set + if (!settings.mainBranch) { + const detectResult = await window.electronAPI.detectMainBranch(project.path); + if (detectResult.success && detectResult.data) { + setSettings(prev => ({ ...prev, mainBranch: detectResult.data! })); + } + } + } + } catch (error) { + console.error('Failed to load branches:', error); + } finally { + setIsLoadingBranches(false); + } + }; + if (!envConfig) return null; return ( @@ -385,6 +434,47 @@ export function IntegrationSettings({ onCheckedChange={(checked) => updateEnvConfig({ githubAutoSync: checked })} /> + + + + {/* Main Branch Selection */} +
+
+ + +
+

+ The base branch for creating task worktrees. All new tasks will branch from here. +

+ + {settings.mainBranch && ( +

+ Tasks will be created on branches like auto-claude/task-name from {settings.mainBranch} +

+ )} +
)} diff --git a/auto-claude-ui/src/renderer/components/project-settings/ProjectSettings.tsx b/auto-claude-ui/src/renderer/components/project-settings/ProjectSettings.tsx index 8ed5497c..ce37196c 100644 --- a/auto-claude-ui/src/renderer/components/project-settings/ProjectSettings.tsx +++ b/auto-claude-ui/src/renderer/components/project-settings/ProjectSettings.tsx @@ -118,6 +118,9 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings ({ success: true, data: [] + }), + + // Git operations + getGitBranches: async () => ({ + success: true, + data: ['main', 'develop', 'feature/test'] + }), + + getCurrentGitBranch: async () => ({ + success: true, + data: 'main' + }), + + detectMainBranch: async () => ({ + success: true, + data: 'main' }) }; diff --git a/auto-claude-ui/src/shared/constants/ipc.ts b/auto-claude-ui/src/shared/constants/ipc.ts index 5a55f8f7..7dfb1c03 100644 --- a/auto-claude-ui/src/shared/constants/ipc.ts +++ b/auto-claude-ui/src/shared/constants/ipc.ts @@ -246,5 +246,10 @@ export const IPC_CHANNELS = { INSIGHTS_ERROR: 'insights:error', // File explorer operations - FILE_EXPLORER_LIST: 'fileExplorer:list' + FILE_EXPLORER_LIST: 'fileExplorer:list', + + // Git operations + GIT_GET_BRANCHES: 'git:getBranches', + GIT_GET_CURRENT_BRANCH: 'git:getCurrentBranch', + GIT_DETECT_MAIN_BRANCH: 'git:detectMainBranch' } as const; diff --git a/auto-claude-ui/src/shared/types/ipc.ts b/auto-claude-ui/src/shared/types/ipc.ts index 3ac3ec2e..73fe2566 100644 --- a/auto-claude-ui/src/shared/types/ipc.ts +++ b/auto-claude-ui/src/shared/types/ipc.ts @@ -459,6 +459,11 @@ export interface ElectronAPI { // File explorer operations listDirectory: (dirPath: string) => Promise>; + + // Git operations + getGitBranches: (projectPath: string) => Promise>; + getCurrentGitBranch: (projectPath: string) => Promise>; + detectMainBranch: (projectPath: string) => Promise>; } declare global { diff --git a/auto-claude-ui/src/shared/types/project.ts b/auto-claude-ui/src/shared/types/project.ts index ad9675a0..840cbcbf 100644 --- a/auto-claude-ui/src/shared/types/project.ts +++ b/auto-claude-ui/src/shared/types/project.ts @@ -22,6 +22,8 @@ export interface ProjectSettings { graphitiMcpEnabled: boolean; /** Graphiti MCP server URL (default: http://localhost:8000/mcp/) */ graphitiMcpUrl?: string; + /** Main branch name for worktree creation (default: auto-detected or 'main') */ + mainBranch?: string; } export interface NotificationSettings { diff --git a/auto-claude/cli/build_commands.py b/auto-claude/cli/build_commands.py index d46c7fed..dfd45294 100644 --- a/auto-claude/cli/build_commands.py +++ b/auto-claude/cli/build_commands.py @@ -60,6 +60,7 @@ def handle_build_command( auto_continue: bool, skip_qa: bool, force_bypass_approval: bool, + base_branch: str | None = None, ) -> None: """ Handle the main build command. @@ -75,6 +76,7 @@ def handle_build_command( auto_continue: Auto-continue mode (non-interactive) skip_qa: Skip automatic QA validation force_bypass_approval: Force bypass approval check + base_branch: Base branch for worktree creation (default: current branch) """ # Lazy imports to avoid loading heavy modules from agent import run_autonomous_agent, sync_plan_to_source @@ -182,7 +184,8 @@ def handle_build_command( source_spec_dir = spec_dir working_dir, worktree_manager, localized_spec_dir = setup_workspace( - project_dir, spec_dir.name, workspace_mode, source_spec_dir=spec_dir + project_dir, spec_dir.name, workspace_mode, source_spec_dir=spec_dir, + base_branch=base_branch ) # Use the localized spec directory (inside worktree) for AI access if localized_spec_dir: diff --git a/auto-claude/cli/main.py b/auto-claude/cli/main.py index 8b7bb25f..1135bd94 100644 --- a/auto-claude/cli/main.py +++ b/auto-claude/cli/main.py @@ -229,6 +229,14 @@ Environment Variables: help="Skip approval check and start build anyway (for debugging)", ) + # Base branch for worktree creation + parser.add_argument( + "--base-branch", + type=str, + default=None, + help="Base branch for creating worktrees (default: auto-detect or current branch)", + ) + return parser.parse_args() @@ -366,6 +374,7 @@ def main() -> None: auto_continue=args.auto_continue, skip_qa=args.skip_qa, force_bypass_approval=args.force, + base_branch=args.base_branch, ) diff --git a/auto-claude/core/workspace/setup.py b/auto-claude/core/workspace/setup.py index b1dabeda..5f1505d3 100644 --- a/auto-claude/core/workspace/setup.py +++ b/auto-claude/core/workspace/setup.py @@ -184,6 +184,7 @@ def setup_workspace( spec_name: str, mode: WorkspaceMode, source_spec_dir: Path | None = None, + base_branch: str | None = None, ) -> tuple[Path, WorktreeManager | None, Path | None]: """ Set up the workspace based on user's choice. @@ -195,6 +196,7 @@ def setup_workspace( spec_name: Name of the spec being built (e.g., "001-feature-name") mode: The workspace mode to use source_spec_dir: Optional source spec directory to copy to worktree + base_branch: Base branch for worktree creation (default: current branch) Returns: Tuple of (working_directory, worktree_manager or None, localized_spec_dir or None) @@ -215,7 +217,7 @@ def setup_workspace( # Ensure timeline tracking hook is installed (once per session) ensure_timeline_hook_installed(project_dir) - manager = WorktreeManager(project_dir) + manager = WorktreeManager(project_dir, base_branch=base_branch) manager.setup() # Get or create worktree for THIS SPECIFIC SPEC