autopilot-code 0.0.16 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/run_autopilot.py +144 -1
package/package.json
CHANGED
package/scripts/run_autopilot.py
CHANGED
|
@@ -329,7 +329,7 @@ def check_pr_for_issue(cfg: RepoConfig, issue_number: int) -> dict[str, Any] | N
|
|
|
329
329
|
"--head",
|
|
330
330
|
branch,
|
|
331
331
|
"--json",
|
|
332
|
-
"number,title,state,mergeable,mergeStateStatus,url,merged,closed",
|
|
332
|
+
"number,title,state,mergeable,mergeStateStatus,url,merged,closed,headRefName",
|
|
333
333
|
"--limit",
|
|
334
334
|
"1",
|
|
335
335
|
]
|
|
@@ -341,6 +341,112 @@ def check_pr_for_issue(cfg: RepoConfig, issue_number: int) -> dict[str, Any] | N
|
|
|
341
341
|
return None
|
|
342
342
|
|
|
343
343
|
|
|
344
|
+
def resolve_merge_conflicts(cfg: RepoConfig, issue_number: int, pr: dict[str, Any], attempt: int) -> bool:
|
|
345
|
+
"""Attempt to resolve merge conflicts using the coding agent. Returns True if resolved."""
|
|
346
|
+
if cfg.agent != "opencode":
|
|
347
|
+
print(f"[{cfg.repo}] Cannot resolve conflicts: agent '{cfg.agent}' not supported (only 'opencode')", flush=True)
|
|
348
|
+
return False
|
|
349
|
+
|
|
350
|
+
pr_number = pr["number"]
|
|
351
|
+
branch = pr.get("headRefName", f"autopilot/issue-{issue_number}")
|
|
352
|
+
worktree = Path(f"/tmp/autopilot-issue-{issue_number}")
|
|
353
|
+
|
|
354
|
+
print(f"[{cfg.repo}] Attempting to resolve merge conflicts for PR #{pr_number} (attempt {attempt}/{cfg.conflict_resolution_max_attempts})", flush=True)
|
|
355
|
+
|
|
356
|
+
# Ensure worktree exists and is up to date
|
|
357
|
+
if not worktree.exists():
|
|
358
|
+
print(f"[{cfg.repo}] Creating worktree at {worktree}", flush=True)
|
|
359
|
+
try:
|
|
360
|
+
sh(["git", "worktree", "add", str(worktree), branch], cwd=cfg.root)
|
|
361
|
+
except Exception as e:
|
|
362
|
+
print(f"[{cfg.repo}] Failed to create worktree: {e}", flush=True)
|
|
363
|
+
return False
|
|
364
|
+
|
|
365
|
+
# Fetch latest and attempt rebase/merge
|
|
366
|
+
try:
|
|
367
|
+
sh(["git", "fetch", "origin", "main"], cwd=worktree)
|
|
368
|
+
|
|
369
|
+
# Try to rebase onto main
|
|
370
|
+
rebase_result = subprocess.run(
|
|
371
|
+
["git", "rebase", "origin/main"],
|
|
372
|
+
cwd=str(worktree),
|
|
373
|
+
capture_output=True,
|
|
374
|
+
text=True
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
if rebase_result.returncode == 0:
|
|
378
|
+
# No conflicts, push and we're done
|
|
379
|
+
sh(["git", "push", "-f", "origin", branch], cwd=worktree)
|
|
380
|
+
print(f"[{cfg.repo}] Rebase successful, no conflicts to resolve", flush=True)
|
|
381
|
+
return True
|
|
382
|
+
|
|
383
|
+
# There are conflicts - abort the rebase first
|
|
384
|
+
subprocess.run(["git", "rebase", "--abort"], cwd=str(worktree), capture_output=True)
|
|
385
|
+
|
|
386
|
+
# Get list of files that would conflict
|
|
387
|
+
merge_base = sh(["git", "merge-base", "HEAD", "origin/main"], cwd=worktree).strip()
|
|
388
|
+
diff_output = sh(["git", "diff", "--name-only", merge_base, "origin/main"], cwd=worktree, check=False)
|
|
389
|
+
conflicting_files = diff_output.strip().split("\n") if diff_output.strip() else []
|
|
390
|
+
|
|
391
|
+
# Build prompt for the agent
|
|
392
|
+
conflict_prompt = f"""This PR has merge conflicts with main. Please resolve them.
|
|
393
|
+
|
|
394
|
+
Branch: {branch}
|
|
395
|
+
Files potentially conflicting: {', '.join(conflicting_files[:10])}
|
|
396
|
+
|
|
397
|
+
Steps:
|
|
398
|
+
1. Run `git fetch origin main`
|
|
399
|
+
2. Run `git rebase origin/main` to start the rebase
|
|
400
|
+
3. When conflicts occur, examine the conflicted files
|
|
401
|
+
4. Edit the files to resolve conflicts (remove conflict markers, keep correct code)
|
|
402
|
+
5. Run `git add <resolved-files>` for each resolved file
|
|
403
|
+
6. Run `git rebase --continue`
|
|
404
|
+
7. Repeat until rebase is complete
|
|
405
|
+
8. Run `git push -f origin {branch}` to update the PR
|
|
406
|
+
|
|
407
|
+
Work rules:
|
|
408
|
+
- Focus only on resolving conflicts, don't make unrelated changes
|
|
409
|
+
- Preserve the intent of both the PR changes and main branch changes
|
|
410
|
+
- If unsure about a conflict, prefer the PR's changes but ensure compatibility
|
|
411
|
+
"""
|
|
412
|
+
|
|
413
|
+
print(f"[{cfg.repo}] Running opencode to resolve conflicts...", flush=True)
|
|
414
|
+
|
|
415
|
+
# Run opencode in the worktree
|
|
416
|
+
agent_result = subprocess.run(
|
|
417
|
+
["opencode", "run", conflict_prompt],
|
|
418
|
+
cwd=str(worktree),
|
|
419
|
+
capture_output=True,
|
|
420
|
+
text=True,
|
|
421
|
+
timeout=600 # 10 minute timeout
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
if agent_result.returncode != 0:
|
|
425
|
+
print(f"[{cfg.repo}] Opencode failed: {agent_result.stderr[:500]}", flush=True)
|
|
426
|
+
return False
|
|
427
|
+
|
|
428
|
+
# Check if conflicts are resolved by checking PR status
|
|
429
|
+
time.sleep(5) # Give GitHub time to update
|
|
430
|
+
cmd = ["gh", "pr", "view", str(pr_number), "--repo", cfg.repo, "--json", "mergeable,mergeStateStatus"]
|
|
431
|
+
pr_status = json.loads(sh(cmd))
|
|
432
|
+
|
|
433
|
+
if pr_status.get("mergeable") == "MERGEABLE":
|
|
434
|
+
print(f"[{cfg.repo}] Conflicts resolved successfully!", flush=True)
|
|
435
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo,
|
|
436
|
+
"--body", f"✅ Autopilot resolved merge conflicts (attempt {attempt})."])
|
|
437
|
+
return True
|
|
438
|
+
else:
|
|
439
|
+
print(f"[{cfg.repo}] Conflicts still present after resolution attempt", flush=True)
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
except subprocess.TimeoutExpired:
|
|
443
|
+
print(f"[{cfg.repo}] Conflict resolution timed out", flush=True)
|
|
444
|
+
return False
|
|
445
|
+
except Exception as e:
|
|
446
|
+
print(f"[{cfg.repo}] Error resolving conflicts: {e}", flush=True)
|
|
447
|
+
return False
|
|
448
|
+
|
|
449
|
+
|
|
344
450
|
def try_merge_pr(cfg: RepoConfig, issue_number: int, pr: dict[str, Any]) -> bool:
|
|
345
451
|
"""Attempt to merge a PR if it's ready. Returns True if merged or closed."""
|
|
346
452
|
pr_number = pr["number"]
|
|
@@ -358,6 +464,43 @@ def try_merge_pr(cfg: RepoConfig, issue_number: int, pr: dict[str, Any]) -> bool
|
|
|
358
464
|
mergeable = pr.get("mergeable")
|
|
359
465
|
merge_state = pr.get("mergeStateStatus")
|
|
360
466
|
|
|
467
|
+
# Handle merge conflicts
|
|
468
|
+
if mergeable == "CONFLICTING" or merge_state == "DIRTY":
|
|
469
|
+
print(f"[{cfg.repo}] PR #{pr_number} has merge conflicts", flush=True)
|
|
470
|
+
|
|
471
|
+
if not cfg.auto_resolve_conflicts:
|
|
472
|
+
print(f"[{cfg.repo}] Auto-resolve conflicts disabled, skipping", flush=True)
|
|
473
|
+
return False
|
|
474
|
+
|
|
475
|
+
# Load state to track resolution attempts
|
|
476
|
+
state = load_state(cfg.root)
|
|
477
|
+
conflict_key = f"conflict_attempts_{issue_number}"
|
|
478
|
+
attempts = state.get(conflict_key, 0)
|
|
479
|
+
|
|
480
|
+
if attempts >= cfg.conflict_resolution_max_attempts:
|
|
481
|
+
print(f"[{cfg.repo}] Max conflict resolution attempts ({cfg.conflict_resolution_max_attempts}) reached", flush=True)
|
|
482
|
+
if attempts == cfg.conflict_resolution_max_attempts:
|
|
483
|
+
# Only notify once when we hit the limit
|
|
484
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo,
|
|
485
|
+
"--body", f"❌ Autopilot failed to resolve merge conflicts after {attempts} attempts. Manual resolution required."])
|
|
486
|
+
state[conflict_key] = attempts + 1 # Increment to avoid repeat notifications
|
|
487
|
+
write_state(cfg.root, state)
|
|
488
|
+
return False
|
|
489
|
+
|
|
490
|
+
# Attempt to resolve
|
|
491
|
+
attempts += 1
|
|
492
|
+
state[conflict_key] = attempts
|
|
493
|
+
write_state(cfg.root, state)
|
|
494
|
+
|
|
495
|
+
if resolve_merge_conflicts(cfg, issue_number, pr, attempts):
|
|
496
|
+
# Reset attempts on success
|
|
497
|
+
del state[conflict_key]
|
|
498
|
+
write_state(cfg.root, state)
|
|
499
|
+
# Re-check the PR - it might be ready to merge now
|
|
500
|
+
return False # Return False to re-check on next cycle
|
|
501
|
+
else:
|
|
502
|
+
return False
|
|
503
|
+
|
|
361
504
|
if mergeable != "MERGEABLE":
|
|
362
505
|
print(f"[{cfg.repo}] PR #{pr_number} is not mergeable (state: {mergeable}/{merge_state}), skipping", flush=True)
|
|
363
506
|
return False
|