1212import os
1313import re
1414import subprocess
15+ import uuid
1516
1617from swe_af .reasoners import router
1718from swe_af .reasoners .pipeline import _assign_sequence_numbers , _compute_levels , _validate_file_conflicts
@@ -76,7 +77,8 @@ async def build(
7677 raise ValueError ("Either repo_path or repo_url must be provided" )
7778
7879 # Clone if repo_url is set and target doesn't exist yet
79- 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 ):
8082 app .note (f"Cloning { cfg .repo_url } → { repo_path } " , tags = ["build" , "clone" ])
8183 os .makedirs (repo_path , exist_ok = True )
8284 clone_result = subprocess .run (
@@ -88,6 +90,58 @@ async def build(
8890 err = clone_result .stderr .strip ()
8991 app .note (f"Clone failed (exit { clone_result .returncode } ): { err } " , tags = ["build" , "clone" , "error" ])
9092 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 } " )
91145 else :
92146 # Ensure repo_path exists even when no repo_url is provided (fresh init case)
93147 # This is needed because planning agents may need to read the repo in parallel with git_init
@@ -105,7 +159,11 @@ async def build(
105159 # Resolve runtime + flat model config once for this build.
106160 resolved = cfg .resolved_models ()
107161
108- app .note ("Build starting" , tags = ["build" , "start" ])
162+ # Unique ID for this build — namespaces git branches/worktrees to prevent
163+ # collisions when multiple builds run concurrently on the same repository.
164+ build_id = uuid .uuid4 ().hex [:8 ]
165+
166+ app .note (f"Build starting (build_id={ build_id } )" , tags = ["build" , "start" ])
109167
110168 # Compute absolute artifacts directory path for logging
111169 abs_artifacts_dir = os .path .join (os .path .abspath (repo_path ), artifacts_dir )
@@ -151,6 +209,7 @@ async def build(
151209 permission_mode = cfg .permission_mode ,
152210 ai_provider = cfg .ai_provider ,
153211 previous_error = previous_error ,
212+ build_id = build_id ,
154213 )
155214
156215 # Run planning only on first attempt, then just git_init on retries
@@ -223,6 +282,7 @@ async def build(
223282 execute_fn_target = cfg .execute_fn_target ,
224283 config = exec_config ,
225284 git_config = git_config ,
285+ build_id = build_id ,
226286 ), "execute" )
227287
228288 # 3. VERIFY
@@ -634,6 +694,7 @@ async def execute(
634694 config : dict | None = None ,
635695 git_config : dict | None = None ,
636696 resume : bool = False ,
697+ build_id : str = "" ,
637698) -> dict :
638699 """Execute a planned DAG with self-healing replanning.
639700
@@ -675,6 +736,7 @@ async def execute_fn(issue, dag_state):
675736 node_id = NODE_ID ,
676737 git_config = git_config ,
677738 resume = resume ,
739+ build_id = build_id ,
678740 )
679741 return state .model_dump ()
680742
0 commit comments