autopilot-code 0.0.16 → 0.0.18
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 +179 -1
- package/scripts/run_opencode_issue.sh +45 -3
package/package.json
CHANGED
package/scripts/run_autopilot.py
CHANGED
|
@@ -24,6 +24,7 @@ import time
|
|
|
24
24
|
from dataclasses import dataclass
|
|
25
25
|
from pathlib import Path
|
|
26
26
|
from typing import Any
|
|
27
|
+
import shutil
|
|
27
28
|
|
|
28
29
|
STATE_DIR = ".autopilot"
|
|
29
30
|
STATE_FILE = "state.json"
|
|
@@ -329,7 +330,7 @@ def check_pr_for_issue(cfg: RepoConfig, issue_number: int) -> dict[str, Any] | N
|
|
|
329
330
|
"--head",
|
|
330
331
|
branch,
|
|
331
332
|
"--json",
|
|
332
|
-
"number,title,state,mergeable,mergeStateStatus,url,merged,closed",
|
|
333
|
+
"number,title,state,mergeable,mergeStateStatus,url,merged,closed,headRefName",
|
|
333
334
|
"--limit",
|
|
334
335
|
"1",
|
|
335
336
|
]
|
|
@@ -341,6 +342,146 @@ def check_pr_for_issue(cfg: RepoConfig, issue_number: int) -> dict[str, Any] | N
|
|
|
341
342
|
return None
|
|
342
343
|
|
|
343
344
|
|
|
345
|
+
def find_opencode() -> str | None:
|
|
346
|
+
"""Find opencode binary in PATH or common locations."""
|
|
347
|
+
# 1. Check PATH first
|
|
348
|
+
opencode = shutil.which("opencode")
|
|
349
|
+
if opencode:
|
|
350
|
+
return opencode
|
|
351
|
+
|
|
352
|
+
# 2. Check common nvm locations
|
|
353
|
+
home = Path.home()
|
|
354
|
+
nvm_dir = home / ".nvm" / "versions" / "node"
|
|
355
|
+
if nvm_dir.exists():
|
|
356
|
+
for node_version in sorted(nvm_dir.iterdir(), reverse=True):
|
|
357
|
+
candidate = node_version / "bin" / "opencode"
|
|
358
|
+
if candidate.exists() and candidate.is_file():
|
|
359
|
+
return str(candidate)
|
|
360
|
+
|
|
361
|
+
# 3. Other common locations
|
|
362
|
+
for path in [
|
|
363
|
+
home / ".local" / "bin" / "opencode",
|
|
364
|
+
Path("/usr/local/bin/opencode"),
|
|
365
|
+
home / ".npm-global" / "bin" / "opencode",
|
|
366
|
+
]:
|
|
367
|
+
if path.exists() and path.is_file():
|
|
368
|
+
return str(path)
|
|
369
|
+
|
|
370
|
+
return None
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def resolve_merge_conflicts(cfg: RepoConfig, issue_number: int, pr: dict[str, Any], attempt: int) -> bool:
|
|
374
|
+
"""Attempt to resolve merge conflicts using the coding agent. Returns True if resolved."""
|
|
375
|
+
if cfg.agent != "opencode":
|
|
376
|
+
print(f"[{cfg.repo}] Cannot resolve conflicts: agent '{cfg.agent}' not supported (only 'opencode')", flush=True)
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
opencode_bin = find_opencode()
|
|
380
|
+
if not opencode_bin:
|
|
381
|
+
print(f"[{cfg.repo}] Cannot resolve conflicts: opencode not found", flush=True)
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
pr_number = pr["number"]
|
|
385
|
+
branch = pr.get("headRefName", f"autopilot/issue-{issue_number}")
|
|
386
|
+
worktree = Path(f"/tmp/autopilot-issue-{issue_number}")
|
|
387
|
+
|
|
388
|
+
print(f"[{cfg.repo}] Attempting to resolve merge conflicts for PR #{pr_number} (attempt {attempt}/{cfg.conflict_resolution_max_attempts})", flush=True)
|
|
389
|
+
print(f"[{cfg.repo}] Using opencode: {opencode_bin}", flush=True)
|
|
390
|
+
|
|
391
|
+
# Ensure worktree exists and is up to date
|
|
392
|
+
if not worktree.exists():
|
|
393
|
+
print(f"[{cfg.repo}] Creating worktree at {worktree}", flush=True)
|
|
394
|
+
try:
|
|
395
|
+
sh(["git", "worktree", "add", str(worktree), branch], cwd=cfg.root)
|
|
396
|
+
except Exception as e:
|
|
397
|
+
print(f"[{cfg.repo}] Failed to create worktree: {e}", flush=True)
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
# Fetch latest and attempt rebase/merge
|
|
401
|
+
try:
|
|
402
|
+
sh(["git", "fetch", "origin", "main"], cwd=worktree)
|
|
403
|
+
|
|
404
|
+
# Try to rebase onto main
|
|
405
|
+
rebase_result = subprocess.run(
|
|
406
|
+
["git", "rebase", "origin/main"],
|
|
407
|
+
cwd=str(worktree),
|
|
408
|
+
capture_output=True,
|
|
409
|
+
text=True
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
if rebase_result.returncode == 0:
|
|
413
|
+
# No conflicts, push and we're done
|
|
414
|
+
sh(["git", "push", "-f", "origin", branch], cwd=worktree)
|
|
415
|
+
print(f"[{cfg.repo}] Rebase successful, no conflicts to resolve", flush=True)
|
|
416
|
+
return True
|
|
417
|
+
|
|
418
|
+
# There are conflicts - abort the rebase first
|
|
419
|
+
subprocess.run(["git", "rebase", "--abort"], cwd=str(worktree), capture_output=True)
|
|
420
|
+
|
|
421
|
+
# Get list of files that would conflict
|
|
422
|
+
merge_base = sh(["git", "merge-base", "HEAD", "origin/main"], cwd=worktree).strip()
|
|
423
|
+
diff_output = sh(["git", "diff", "--name-only", merge_base, "origin/main"], cwd=worktree, check=False)
|
|
424
|
+
conflicting_files = diff_output.strip().split("\n") if diff_output.strip() else []
|
|
425
|
+
|
|
426
|
+
# Build prompt for the agent
|
|
427
|
+
conflict_prompt = f"""This PR has merge conflicts with main. Please resolve them.
|
|
428
|
+
|
|
429
|
+
Branch: {branch}
|
|
430
|
+
Files potentially conflicting: {', '.join(conflicting_files[:10])}
|
|
431
|
+
|
|
432
|
+
Steps:
|
|
433
|
+
1. Run `git fetch origin main`
|
|
434
|
+
2. Run `git rebase origin/main` to start the rebase
|
|
435
|
+
3. When conflicts occur, examine the conflicted files
|
|
436
|
+
4. Edit the files to resolve conflicts (remove conflict markers, keep correct code)
|
|
437
|
+
5. Run `git add <resolved-files>` for each resolved file
|
|
438
|
+
6. Run `git rebase --continue`
|
|
439
|
+
7. Repeat until rebase is complete
|
|
440
|
+
8. Run `git push -f origin {branch}` to update the PR
|
|
441
|
+
|
|
442
|
+
Work rules:
|
|
443
|
+
- Focus only on resolving conflicts, don't make unrelated changes
|
|
444
|
+
- Preserve the intent of both the PR changes and main branch changes
|
|
445
|
+
- If unsure about a conflict, prefer the PR's changes but ensure compatibility
|
|
446
|
+
"""
|
|
447
|
+
|
|
448
|
+
print(f"[{cfg.repo}] Running opencode to resolve conflicts...", flush=True)
|
|
449
|
+
|
|
450
|
+
# Run opencode in the worktree
|
|
451
|
+
agent_result = subprocess.run(
|
|
452
|
+
[opencode_bin, "run", conflict_prompt],
|
|
453
|
+
cwd=str(worktree),
|
|
454
|
+
capture_output=True,
|
|
455
|
+
text=True,
|
|
456
|
+
timeout=600 # 10 minute timeout
|
|
457
|
+
)
|
|
458
|
+
|
|
459
|
+
if agent_result.returncode != 0:
|
|
460
|
+
print(f"[{cfg.repo}] Opencode failed: {agent_result.stderr[:500]}", flush=True)
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
# Check if conflicts are resolved by checking PR status
|
|
464
|
+
time.sleep(5) # Give GitHub time to update
|
|
465
|
+
cmd = ["gh", "pr", "view", str(pr_number), "--repo", cfg.repo, "--json", "mergeable,mergeStateStatus"]
|
|
466
|
+
pr_status = json.loads(sh(cmd))
|
|
467
|
+
|
|
468
|
+
if pr_status.get("mergeable") == "MERGEABLE":
|
|
469
|
+
print(f"[{cfg.repo}] Conflicts resolved successfully!", flush=True)
|
|
470
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo,
|
|
471
|
+
"--body", f"✅ Autopilot resolved merge conflicts (attempt {attempt})."])
|
|
472
|
+
return True
|
|
473
|
+
else:
|
|
474
|
+
print(f"[{cfg.repo}] Conflicts still present after resolution attempt", flush=True)
|
|
475
|
+
return False
|
|
476
|
+
|
|
477
|
+
except subprocess.TimeoutExpired:
|
|
478
|
+
print(f"[{cfg.repo}] Conflict resolution timed out", flush=True)
|
|
479
|
+
return False
|
|
480
|
+
except Exception as e:
|
|
481
|
+
print(f"[{cfg.repo}] Error resolving conflicts: {e}", flush=True)
|
|
482
|
+
return False
|
|
483
|
+
|
|
484
|
+
|
|
344
485
|
def try_merge_pr(cfg: RepoConfig, issue_number: int, pr: dict[str, Any]) -> bool:
|
|
345
486
|
"""Attempt to merge a PR if it's ready. Returns True if merged or closed."""
|
|
346
487
|
pr_number = pr["number"]
|
|
@@ -358,6 +499,43 @@ def try_merge_pr(cfg: RepoConfig, issue_number: int, pr: dict[str, Any]) -> bool
|
|
|
358
499
|
mergeable = pr.get("mergeable")
|
|
359
500
|
merge_state = pr.get("mergeStateStatus")
|
|
360
501
|
|
|
502
|
+
# Handle merge conflicts
|
|
503
|
+
if mergeable == "CONFLICTING" or merge_state == "DIRTY":
|
|
504
|
+
print(f"[{cfg.repo}] PR #{pr_number} has merge conflicts", flush=True)
|
|
505
|
+
|
|
506
|
+
if not cfg.auto_resolve_conflicts:
|
|
507
|
+
print(f"[{cfg.repo}] Auto-resolve conflicts disabled, skipping", flush=True)
|
|
508
|
+
return False
|
|
509
|
+
|
|
510
|
+
# Load state to track resolution attempts
|
|
511
|
+
state = load_state(cfg.root)
|
|
512
|
+
conflict_key = f"conflict_attempts_{issue_number}"
|
|
513
|
+
attempts = state.get(conflict_key, 0)
|
|
514
|
+
|
|
515
|
+
if attempts >= cfg.conflict_resolution_max_attempts:
|
|
516
|
+
print(f"[{cfg.repo}] Max conflict resolution attempts ({cfg.conflict_resolution_max_attempts}) reached", flush=True)
|
|
517
|
+
if attempts == cfg.conflict_resolution_max_attempts:
|
|
518
|
+
# Only notify once when we hit the limit
|
|
519
|
+
sh(["gh", "issue", "comment", str(issue_number), "--repo", cfg.repo,
|
|
520
|
+
"--body", f"❌ Autopilot failed to resolve merge conflicts after {attempts} attempts. Manual resolution required."])
|
|
521
|
+
state[conflict_key] = attempts + 1 # Increment to avoid repeat notifications
|
|
522
|
+
write_state(cfg.root, state)
|
|
523
|
+
return False
|
|
524
|
+
|
|
525
|
+
# Attempt to resolve
|
|
526
|
+
attempts += 1
|
|
527
|
+
state[conflict_key] = attempts
|
|
528
|
+
write_state(cfg.root, state)
|
|
529
|
+
|
|
530
|
+
if resolve_merge_conflicts(cfg, issue_number, pr, attempts):
|
|
531
|
+
# Reset attempts on success
|
|
532
|
+
del state[conflict_key]
|
|
533
|
+
write_state(cfg.root, state)
|
|
534
|
+
# Re-check the PR - it might be ready to merge now
|
|
535
|
+
return False # Return False to re-check on next cycle
|
|
536
|
+
else:
|
|
537
|
+
return False
|
|
538
|
+
|
|
361
539
|
if mergeable != "MERGEABLE":
|
|
362
540
|
print(f"[{cfg.repo}] PR #{pr_number} is not mergeable (state: {mergeable}/{merge_state}), skipping", flush=True)
|
|
363
541
|
return False
|
|
@@ -21,6 +21,7 @@ if command -v jq >/dev/null 2>&1; then
|
|
|
21
21
|
AUTO_FIX_CHECKS=$(jq -r '.autoFixChecks // true' < .autopilot/autopilot.json)
|
|
22
22
|
AUTO_FIX_CHECKS_MAX_ATTEMPTS=$(jq -r '.autoFixChecksMaxAttempts // 3' < .autopilot/autopilot.json)
|
|
23
23
|
ALLOWED_USERS=$(jq -r '.allowedMergeUsers[]' < .autopilot/autopilot.json 2>/dev/null || true)
|
|
24
|
+
AGENT_PATH=$(jq -r '.agentPath // ""' < .autopilot/autopilot.json)
|
|
24
25
|
else
|
|
25
26
|
REPO=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["repo"])')
|
|
26
27
|
AUTO_MERGE=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoMerge", True))')
|
|
@@ -30,8 +31,49 @@ else
|
|
|
30
31
|
AUTO_FIX_CHECKS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecks", True))')
|
|
31
32
|
AUTO_FIX_CHECKS_MAX_ATTEMPTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecksMaxAttempts", 3))')
|
|
32
33
|
ALLOWED_USERS=$(python3 -c 'import json,sys; users=json.load(open(".autopilot/autopilot.json")).get("allowedMergeUsers", []); print("\n".join(users))' 2>/dev/null || true)
|
|
34
|
+
AGENT_PATH=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("agentPath", ""))' 2>/dev/null || true)
|
|
33
35
|
fi
|
|
34
36
|
|
|
37
|
+
# Find opencode binary - check config, PATH, then common locations
|
|
38
|
+
find_opencode() {
|
|
39
|
+
# 1. Config-specified path
|
|
40
|
+
if [[ -n "$AGENT_PATH" ]] && [[ -x "$AGENT_PATH" ]]; then
|
|
41
|
+
echo "$AGENT_PATH"
|
|
42
|
+
return
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# 2. Already in PATH
|
|
46
|
+
if command -v opencode >/dev/null 2>&1; then
|
|
47
|
+
command -v opencode
|
|
48
|
+
return
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
# 3. Common nvm locations
|
|
52
|
+
for node_dir in "$HOME/.nvm/versions/node"/*/bin; do
|
|
53
|
+
if [[ -x "$node_dir/opencode" ]]; then
|
|
54
|
+
echo "$node_dir/opencode"
|
|
55
|
+
return
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# 4. Other common locations
|
|
60
|
+
for path in "$HOME/.local/bin/opencode" "/usr/local/bin/opencode" "$HOME/.npm-global/bin/opencode"; do
|
|
61
|
+
if [[ -x "$path" ]]; then
|
|
62
|
+
echo "$path"
|
|
63
|
+
return
|
|
64
|
+
fi
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
# Not found
|
|
68
|
+
return 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
OPENCODE_BIN=$(find_opencode) || {
|
|
72
|
+
echo "[run_opencode_issue.sh] ERROR: opencode not found. Set 'agentPath' in autopilot.json or ensure opencode is installed." >&2
|
|
73
|
+
exit 1
|
|
74
|
+
}
|
|
75
|
+
echo "[run_opencode_issue.sh] Using opencode: $OPENCODE_BIN"
|
|
76
|
+
|
|
35
77
|
WORKTREE="/tmp/autopilot-issue-$ISSUE_NUMBER"
|
|
36
78
|
BRANCH="autopilot/issue-$ISSUE_NUMBER"
|
|
37
79
|
|
|
@@ -73,7 +115,7 @@ Work rules:
|
|
|
73
115
|
- If the issue is a simple file-addition, just do it directly (no extra refactors)."
|
|
74
116
|
# 4. Run opencode inside worktree
|
|
75
117
|
cd "$WORKTREE"
|
|
76
|
-
|
|
118
|
+
"$OPENCODE_BIN" run "$PROMPT"
|
|
77
119
|
|
|
78
120
|
# 5. Commit any changes OpenCode made
|
|
79
121
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
@@ -190,7 +232,7 @@ if [[ "$AUTO_RESOLVE_CONFLICTS" == "true" ]] && [[ -n "$PR_URL" ]]; then
|
|
|
190
232
|
|
|
191
233
|
After resolving all conflicts, report the files that were resolved."
|
|
192
234
|
|
|
193
|
-
if
|
|
235
|
+
if "$OPENCODE_BIN" run "$CONFLICT_PROMPT"; then
|
|
194
236
|
# Check if there are still conflicts
|
|
195
237
|
if [[ -z "$(git diff --name-only --diff-filter=U)" ]]; then
|
|
196
238
|
echo "[run_opencode_issue.sh] Conflicts resolved by agent."
|
|
@@ -316,7 +358,7 @@ if [[ "$AUTO_MERGE" == "true" ]] && [[ -n "$PR_URL" ]]; then
|
|
|
316
358
|
|
|
317
359
|
# Run opencode to fix the issue
|
|
318
360
|
cd "$WORKTREE"
|
|
319
|
-
if
|
|
361
|
+
if "$OPENCODE_BIN" run "$REPAIR_PROMPT"; then
|
|
320
362
|
# Commit any changes
|
|
321
363
|
if [[ -n "$(git status --porcelain)" ]]; then
|
|
322
364
|
git add -A
|