138138"""
139139
140140
141- def run_scout (subproject : str , date : str , repo_root : Path ) -> tuple [str , dict , str ]:
142- """Run a single scout agent in a git worktree. Returns (subproject, result_dict, worktree_path)."""
141+ def setup_scout_worktree (subproject : str , date : str , repo_root : Path ) -> Path :
142+ """Create a fresh worktree for a scout. Must run sequentially — `git worktree add`
143+ serializes on repo metadata and races when called from multiple threads."""
143144 worktree_name = f"nightshift-scout-{ subproject .replace ('/' , '-' )} "
144145 worktree_path = repo_root / ".claude" / "nightshift-worktrees" / worktree_name
145146 branch_name = f"nightshift/scout-{ subproject .replace ('/' , '-' )} -{ date } "
146147
147- # Clean up any stale worktree
148148 subprocess .run (["git" , "worktree" , "remove" , "--force" , str (worktree_path )], capture_output = True , cwd = repo_root )
149149 subprocess .run (["git" , "branch" , "-D" , branch_name ], capture_output = True , cwd = repo_root )
150150
151- # Create worktree on a fresh branch (origin/main already fetched by main())
152151 subprocess .run (
153152 ["git" , "worktree" , "add" , "-b" , branch_name , str (worktree_path ), "origin/main" ],
154153 check = True ,
155154 cwd = repo_root ,
156155 )
156+ return worktree_path
157157
158- with tempfile .NamedTemporaryFile (suffix = ".json" , prefix = f"nightshift-{ worktree_name } -" , delete = False ) as f :
158+
159+ def run_scout (subproject : str , worktree_path : Path ) -> tuple [str , dict , str ]:
160+ """Run a single scout agent in a pre-created git worktree."""
161+ with tempfile .NamedTemporaryFile (suffix = ".json" , prefix = f"nightshift-{ worktree_path .name } -" , delete = False ) as f :
159162 result_file = f .name
160163 haiku_seed = secrets .token_hex (4 )
161164
@@ -175,12 +178,11 @@ def run_scout(subproject: str, date: str, repo_root: Path) -> tuple[str, dict, s
175178 "--tools=Read,Write,Edit,Glob,Grep,Bash" ,
176179 "--max-turns" ,
177180 "400" ,
178- "--cwd" ,
179- str (worktree_path ),
180181 "--" ,
181182 prompt ,
182183 ],
183184 check = False ,
185+ cwd = worktree_path ,
184186 )
185187
186188 result = {"subproject" : subproject , "status" : "error" , "summary" : "Scout did not produce a result file" }
@@ -251,8 +253,13 @@ def main() -> None:
251253 scout_results : list [dict ] = []
252254 worktree_info : list [tuple [str , str ]] = []
253255
256+ # Create worktrees sequentially, then run claude in parallel. `git worktree add`
257+ # serializes on repo metadata, so concurrent creation races and some invocations
258+ # fail with exit 255.
259+ subproject_worktrees = [(sp , setup_scout_worktree (sp , date , repo_root )) for sp in SUBPROJECTS ]
260+
254261 with ThreadPoolExecutor (max_workers = len (SUBPROJECTS )) as pool :
255- futures = {pool .submit (run_scout , sp , date , repo_root ): sp for sp in SUBPROJECTS }
262+ futures = {pool .submit (run_scout , sp , wt ): sp for sp , wt in subproject_worktrees }
256263 for future in as_completed (futures ):
257264 sp = futures [future ]
258265 try :
0 commit comments