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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autopilot-code",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "private": false,
5
5
  "description": "Repo-issue–driven autopilot runner",
6
6
  "license": "MIT",
@@ -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