@@ -77,7 +77,8 @@ async def build(
7777 raise ValueError ("Either repo_path or repo_url must be provided" )
7878
7979 # Clone if repo_url is set and target doesn't exist yet
80- if cfg .repo_url and not os .path .exists (os .path .join (repo_path , ".git" )):
80+ git_dir = os .path .join (repo_path , ".git" )
81+ if cfg .repo_url and not os .path .exists (git_dir ):
8182 app .note (f"Cloning { cfg .repo_url } → { repo_path } " , tags = ["build" , "clone" ])
8283 os .makedirs (repo_path , exist_ok = True )
8384 clone_result = subprocess .run (
@@ -89,6 +90,58 @@ async def build(
8990 err = clone_result .stderr .strip ()
9091 app .note (f"Clone failed (exit { clone_result .returncode } ): { err } " , tags = ["build" , "clone" , "error" ])
9192 raise RuntimeError (f"git clone failed (exit { clone_result .returncode } ): { err } " )
93+ elif cfg .repo_url and os .path .exists (git_dir ):
94+ # Repo already cloned by a prior build — reset to remote default branch
95+ # so git_init creates the integration branch from a clean baseline.
96+ default_branch = cfg .github_pr_base or "main"
97+ app .note (
98+ f"Repo already exists at { repo_path } — resetting to origin/{ default_branch } " ,
99+ tags = ["build" , "clone" , "reset" ],
100+ )
101+
102+ # Remove stale worktrees on disk before touching branches
103+ worktrees_dir = os .path .join (repo_path , ".worktrees" )
104+ if os .path .isdir (worktrees_dir ):
105+ import shutil
106+ shutil .rmtree (worktrees_dir , ignore_errors = True )
107+ subprocess .run (
108+ ["git" , "worktree" , "prune" ],
109+ cwd = repo_path , capture_output = True , text = True ,
110+ )
111+
112+ # Fetch latest remote state
113+ fetch = subprocess .run (
114+ ["git" , "fetch" , "origin" ],
115+ cwd = repo_path , capture_output = True , text = True ,
116+ )
117+ if fetch .returncode != 0 :
118+ app .note (f"git fetch failed: { fetch .stderr .strip ()} " , tags = ["build" , "clone" , "error" ])
119+
120+ # Force-checkout default branch (handles dirty working tree from crashed builds)
121+ subprocess .run (
122+ ["git" , "checkout" , "-f" , default_branch ],
123+ cwd = repo_path , capture_output = True , text = True ,
124+ )
125+ reset = subprocess .run (
126+ ["git" , "reset" , "--hard" , f"origin/{ default_branch } " ],
127+ cwd = repo_path , capture_output = True , text = True ,
128+ )
129+ if reset .returncode != 0 :
130+ # Hard reset failed — nuke and re-clone as last resort
131+ app .note (
132+ f"Reset to origin/{ default_branch } failed — re-cloning" ,
133+ tags = ["build" , "clone" , "reclone" ],
134+ )
135+ import shutil
136+ shutil .rmtree (repo_path , ignore_errors = True )
137+ os .makedirs (repo_path , exist_ok = True )
138+ clone_result = subprocess .run (
139+ ["git" , "clone" , cfg .repo_url , repo_path ],
140+ capture_output = True , text = True ,
141+ )
142+ if clone_result .returncode != 0 :
143+ err = clone_result .stderr .strip ()
144+ raise RuntimeError (f"git re-clone failed: { err } " )
92145 else :
93146 # Ensure repo_path exists even when no repo_url is provided (fresh init case)
94147 # This is needed because planning agents may need to read the repo in parallel with git_init
0 commit comments