Skip to content

[nightshift] Parallel sub-agents, automerge, and reviewer selection#4391

Merged
rjpower merged 3 commits intomainfrom
claude/suspicious-hertz
Apr 3, 2026
Merged

[nightshift] Parallel sub-agents, automerge, and reviewer selection#4391
rjpower merged 3 commits intomainfrom
claude/suspicious-hertz

Conversation

@rjpower
Copy link
Copy Markdown
Collaborator

@rjpower rjpower commented Apr 3, 2026

Rewrite nightshift_cleanup.py to spawn one scout agent per subproject in
parallel git worktrees, then merge results into a single PR. Each scout
gets 400 turns to find one meaty improvement in its assigned subproject,
then a merge agent cherry-picks all results, runs lint+tests, and opens
one unified PR.

Also add to both cleanup and doc-drift workflows: rebase on origin/main
before pushing, enable automerge via gh pr merge --auto --squash, and
select a reviewer from git log on changed files. Cleanup workflow timeout
bumped from 240 to 360 minutes for parallel agent overhead.

Rewrite nightshift_cleanup.py to spawn one scout agent per subproject in
parallel git worktrees, then merge results into a single PR. Add rebase on
origin/main, gh pr merge --auto --squash, and git-log-based reviewer
selection to both cleanup and doc-drift workflows. Bump cleanup timeout
to 360 minutes.
@rjpower rjpower added the agent-generated Created by automation/agent label Apr 3, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 3, 2026

Claude finished @rjpower's task in 1m 37s —— View job


PR Review: Parallel sub-agents, automerge, and reviewer selection

  • Read changed files and gather context
  • Review code for correctness and issues
  • Post review feedback

Summary

Good architectural change — splitting into parallel scout agents per subproject with a merge agent is a solid design that should improve coverage and reduce wasted turns. The doc-drift automerge/reviewer additions are clean. I left 7 inline comments on nightshift_cleanup.py; here's the high-level:

Issues to address

  1. tempfile.mktemp is deprecated (line 156) — TOCTOU race. Use NamedTemporaryFile(delete=False) instead.

  2. Redundant git fetch in each scout (line 149) — main() already fetches at line 236 before spawning scouts. Running 4 concurrent fetches of the same ref is wasteful.

  3. Unhandled scout exceptions kill the entire run (lines 246-248) — If one scout's worktree setup fails (check=True), future.result() re-raises and aborts all other scouts. Wrap with try/except so surviving scouts still produce results.

  4. Unhandled json.JSONDecodeError (line 184) — If a scout writes malformed JSON, the run crashes. Needs a try/except.

  5. .nightshift-worktrees not in .gitignore (line 141) — If cleanup fails partway, this directory could end up tracked.

  6. cleanup_worktrees fragile on partial failure (lines 221-227) — If git worktree remove fails for one child, rmdir() raises OSError. Consider shutil.rmtree as fallback.

  7. Reviewer selection uses email, but --add-reviewer expects username (line 128) — git log --format='%ae' returns emails, not GitHub usernames. This will fail for most contributors. Need an email→username mapping step (e.g. via gh api).

Design observations

  • 400 turns per scout × 4 scouts is substantial. The workflow timeout bump to 360 minutes should cover it, but worth monitoring actual runtimes.
  • The merge agent max-turns dropped from 800 → 200, which seems reasonable for cherry-pick + PR creation.
  • The doc-drift changes are straightforward prompt additions — no concerns there.

Comment thread infra/scripts/nightshift_cleanup.py Outdated
cwd=repo_root,
)

result_file = tempfile.mktemp(suffix=".json", prefix=f"nightshift-{worktree_name}-")
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tempfile.mktemp is deprecated and has a TOCTOU race — the path could be claimed between creation and the scout writing to it. Use tempfile.NamedTemporaryFile(delete=False) instead:

Suggested change
result_file = tempfile.mktemp(suffix=".json", prefix=f"nightshift-{worktree_name}-")
with tempfile.NamedTemporaryFile(suffix=".json", prefix=f"nightshift-{worktree_name}-", delete=False) as f:
result_file = f.name

Comment thread infra/scripts/nightshift_cleanup.py Outdated
subprocess.run(["git", "branch", "-D", branch_name], capture_output=True, cwd=repo_root)

# Create worktree on a fresh branch from origin/main
subprocess.run(["git", "fetch", "origin", "main"], check=True, cwd=repo_root, capture_output=True)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each of the 4 scouts runs git fetch origin main independently. Since they execute in parallel via ThreadPoolExecutor, this creates 4 concurrent fetches of the same ref. The fetch already happens once in main() at line 236 before scouts are launched — this one could be removed (or guarded with a note that the worktree creation depends on origin/main being fresh).

Comment thread infra/scripts/nightshift_cleanup.py Outdated
Comment on lines +246 to +248
futures = {pool.submit(run_scout, sp, date, repo_root): sp for sp in SUBPROJECTS}
for future in as_completed(futures):
subproject, result, worktree_path = future.result()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any scout raises an exception (e.g. the worktree git checkout fails with check=True), future.result() will re-raise it here and kill the entire run — including any scouts still in-flight. Consider wrapping with try/except to let other scouts finish:

for future in as_completed(futures):
    try:
        subproject, result, worktree_path = future.result()
    except Exception:
        sp = futures[future]
        logger.exception("Scout %s failed", sp)
        scout_results.append({"subproject": sp, "status": "error", "summary": "Scout raised an exception"})
        continue
    ...

)

result = {"subproject": subproject, "status": "error", "summary": "Scout did not produce a result file"}
if Path(result_file).exists():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Malformed JSON from the scout will crash with an unhandled json.JSONDecodeError. Worth wrapping:

Suggested change
if Path(result_file).exists():
if Path(result_file).exists():
try:
result = json.loads(Path(result_file).read_text())
except json.JSONDecodeError:
result = {"subproject": subproject, "status": "error", "summary": "Scout produced invalid JSON"}
Path(result_file).unlink()

Comment thread infra/scripts/nightshift_cleanup.py Outdated
def run_scout(subproject: str, date: str, repo_root: Path) -> tuple[str, dict, str]:
"""Run a single scout agent in a git worktree. Returns (subproject, result_dict, worktree_path)."""
worktree_name = f"nightshift-scout-{subproject.replace('/', '-')}"
worktree_path = repo_root / ".nightshift-worktrees" / worktree_name
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.nightshift-worktrees is not in .gitignore. If a scout or the cleanup function fails partway through, this directory (or its git metadata) could end up tracked. Consider adding it to .gitignore.

Comment thread infra/scripts/nightshift_cleanup.py Outdated
Comment on lines +221 to +227
def cleanup_worktrees(repo_root: Path) -> None:
"""Remove all nightshift scout worktrees."""
worktrees_dir = repo_root / ".nightshift-worktrees"
if worktrees_dir.exists():
for child in worktrees_dir.iterdir():
subprocess.run(["git", "worktree", "remove", "--force", str(child)], capture_output=True, cwd=repo_root)
worktrees_dir.rmdir()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any child directory inside .nightshift-worktrees was not a worktree (or was already removed), worktrees_dir.rmdir() will silently succeed. But if git worktree remove fails for one child, rmdir() will raise OSError because the directory isn't empty. Consider using shutil.rmtree as a fallback, or at least logging failures from the worktree-remove loop.

6. Pick a reviewer by finding who recently touched the changed files:
```
git log --format='%ae' -20 -- <changed_files> | sort | uniq -c | sort -rn | head -5
```
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reviewer selection uses --format='%ae' (email), but gh pr edit --add-reviewer expects a GitHub username, not an email. This will work only if the email happens to match the username. You'd need to map emails → GitHub usernames (e.g. via gh api or by using --format='%aN' and matching against known contributors).

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8aad18d3c7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread infra/scripts/nightshift_cleanup.py Outdated

6. Pick a reviewer by finding who recently touched the changed files:
```
git log --format='%ae' -20 -- <changed_files> | sort | uniq -c | sort -rn | head -5
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Use GitHub logins for reviewer assignment

This workflow instructs the agent to select reviewers from git log --format='%ae', which yields email addresses, but gh pr edit --add-reviewer accepts reviewer logins (per gh pr edit help: --add-reviewer <login>). In practice this causes reviewer assignment to fail (or pick invalid identities), so the new auto-reviewer step in cleanup can break PR automation when the selected value is an email.

Useful? React with 👍 / 👎.

Comment thread infra/scripts/nightshift_cleanup.py Outdated

result = {"subproject": subproject, "status": "error", "summary": "Scout did not produce a result file"}
if Path(result_file).exists():
result = json.loads(Path(result_file).read_text())
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Handle malformed scout JSON instead of crashing merge run

run_scout is intended to tolerate scout failures (check=False on the Claude call), but it unconditionally parses the result file with json.loads(...) and does not catch decode/read errors. If any scout writes partial/invalid JSON, future.result() raises and aborts the whole cleanup run before merge, which defeats the fault-tolerant parallel design.

Useful? React with 👍 / 👎.

rjpower added 2 commits April 3, 2026 09:03
Fix tempfile.mktemp TOCTOU race with NamedTemporaryFile(delete=False).
Remove redundant git fetch from scouts (main() fetches once). Wrap
future.result() to isolate scout failures. Handle malformed scout JSON.
Use shutil.rmtree as fallback in cleanup_worktrees. Switch reviewer
selection from git log emails to gh api GitHub logins. Add
.nightshift-worktrees to .gitignore.
@rjpower rjpower merged commit d3b7c64 into main Apr 3, 2026
36 checks passed
@rjpower rjpower deleted the claude/suspicious-hertz branch April 3, 2026 16:08
Helw150 pushed a commit that referenced this pull request Apr 8, 2026
…4391)

Rewrite nightshift_cleanup.py to spawn one scout agent per subproject in
parallel git worktrees, then merge results into a single PR. Each scout
gets 400 turns to find one meaty improvement in its assigned subproject,
then a merge agent cherry-picks all results, runs lint+tests, and opens
one unified PR.

Also add to both cleanup and doc-drift workflows: rebase on origin/main
before pushing, enable automerge via gh pr merge --auto --squash, and
select a reviewer from git log on changed files. Cleanup workflow
timeout
bumped from 240 to 360 minutes for parallel agent overhead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-generated Created by automation/agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant