Skip to content

Commit 003cf2e

Browse files
authored
[nightshift] Fix scout worktree race and bogus --cwd flag (#4581)
Scout worktrees are now created sequentially before dispatching the claude subprocesses in parallel; concurrent git worktree add races on repo metadata and exits 255, which was killing two of four scouts every night. Also drop the nonexistent --cwd claude CLI flag and pass cwd= to subprocess.run, which was the reason the surviving scouts produced no output.
1 parent d840d6b commit 003cf2e

File tree

1 file changed

+15
-8
lines changed

1 file changed

+15
-8
lines changed

infra/scripts/nightshift_cleanup.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,24 +138,27 @@
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

Comments
 (0)