autopilot-code 0.10.0 → 2.0.0

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.
@@ -1,679 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- REPO_DIR="$1"
5
- ISSUE_NUMBER="$2"
6
-
7
- if [[ -z "$REPO_DIR" || -z "$ISSUE_NUMBER" ]]; then
8
- echo "Usage: $0 <repoDir> <issueNumber>" >&2
9
- exit 1
10
- fi
11
-
12
- cd "$REPO_DIR"
13
-
14
- # Read config (prefer jq, fallback to python3)
15
- if command -v jq >/dev/null 2>&1; then
16
- REPO=$(jq -r '.repo' < .autopilot/autopilot.json)
17
- AUTO_MERGE=$(jq -r '.autoMerge // true' < .autopilot/autopilot.json)
18
- MERGE_METHOD=$(jq -r '.mergeMethod // "squash"' < .autopilot/autopilot.json)
19
- AUTO_RESOLVE_CONFLICTS=$(jq -r '.autoResolveConflicts // true' < .autopilot/autopilot.json)
20
- CONFLICT_RESOLUTION_MAX_ATTEMPTS=$(jq -r '.conflictResolutionMaxAttempts // 3' < .autopilot/autopilot.json)
21
- AUTO_FIX_CHECKS=$(jq -r '.autoFixChecks // true' < .autopilot/autopilot.json)
22
- AUTO_FIX_CHECKS_MAX_ATTEMPTS=$(jq -r '.autoFixChecksMaxAttempts // 3' < .autopilot/autopilot.json)
23
- ALLOWED_USERS=$(jq -r '.allowedMergeUsers[]' < .autopilot/autopilot.json 2>/dev/null || true)
24
- AGENT_PATH=$(jq -r '.agentPath // ""' < .autopilot/autopilot.json)
25
- ENABLE_PLANNING_STEP=$(jq -r '.enablePlanningStep // true' < .autopilot/autopilot.json)
26
- else
27
- REPO=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["repo"])')
28
- AUTO_MERGE=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoMerge", True))')
29
- MERGE_METHOD=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("mergeMethod", "squash"))')
30
- AUTO_RESOLVE_CONFLICTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoResolveConflicts", True))')
31
- CONFLICT_RESOLUTION_MAX_ATTEMPTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("conflictResolutionMaxAttempts", 3))')
32
- AUTO_FIX_CHECKS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecks", True))')
33
- AUTO_FIX_CHECKS_MAX_ATTEMPTS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("autoFixChecksMaxAttempts", 3))')
34
- ALLOWED_USERS=$(python3 -c 'import json,sys; users=json.load(open(".autopilot/autopilot.json")).get("allowedMergeUsers", []); print("\n".join(users))' 2>/dev/null || true)
35
- AGENT_PATH=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("agentPath", ""))' 2>/dev/null || true)
36
- ENABLE_PLANNING_STEP=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json")).get("enablePlanningStep", True))' 2>/dev/null || echo "true")
37
- fi
38
-
39
- # Find opencode binary - check config, PATH, then common locations
40
- find_opencode() {
41
- # 1. Config-specified path
42
- if [[ -n "$AGENT_PATH" ]] && [[ -x "$AGENT_PATH" ]]; then
43
- echo "$AGENT_PATH"
44
- return
45
- fi
46
-
47
- # 2. Already in PATH
48
- if command -v opencode >/dev/null 2>&1; then
49
- command -v opencode
50
- return
51
- fi
52
-
53
- # 3. Common nvm locations
54
- for node_dir in "$HOME/.nvm/versions/node"/*/bin; do
55
- if [[ -x "$node_dir/opencode" ]]; then
56
- echo "$node_dir/opencode"
57
- return
58
- fi
59
- done
60
-
61
- # 4. Other common locations
62
- for path in "$HOME/.local/bin/opencode" "/usr/local/bin/opencode" "$HOME/.npm-global/bin/opencode"; do
63
- if [[ -x "$path" ]]; then
64
- echo "$path"
65
- return
66
- fi
67
- done
68
-
69
- # Not found
70
- return 1
71
- }
72
-
73
- OPENCODE_BIN=$(find_opencode) || {
74
- echo "[run_opencode_issue.sh] ERROR: opencode not found. Set 'agentPath' in autopilot.json or ensure opencode is installed." >&2
75
- exit 1
76
- }
77
- echo "[run_opencode_issue.sh] Using opencode: $OPENCODE_BIN"
78
-
79
- WORKTREE="/tmp/autopilot-issue-$ISSUE_NUMBER"
80
- BRANCH="autopilot/issue-$ISSUE_NUMBER"
81
-
82
- echo "[run_opencode_issue.sh] repo=$REPO issue=$ISSUE_NUMBER worktree=$WORKTREE"
83
-
84
- # 1. Create or reuse git worktree from main
85
- # First, prune any stale worktrees (e.g., if directory was deleted but still registered)
86
- git worktree prune 2>/dev/null || true
87
-
88
- if [[ -d "$WORKTREE" ]]; then
89
- echo "[run_opencode_issue.sh] Reusing existing worktree: $WORKTREE"
90
- else
91
- # If branch exists already, add worktree from that branch; otherwise create from main.
92
- if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
93
- git worktree add "$WORKTREE" "$BRANCH"
94
- else
95
- git worktree add "$WORKTREE" -b "$BRANCH" main
96
- fi
97
- fi
98
-
99
- # 1.5. Install dependencies if package.json exists (for Node.js projects)
100
- if [[ -f "$WORKTREE/package.json" ]]; then
101
- echo "[run_opencode_issue.sh] Installing npm dependencies..."
102
- (cd "$WORKTREE" && npm install --silent) || true
103
- fi
104
-
105
- # 2. Fetch issue title/body
106
- ISSUE_JSON=$(gh issue view "$ISSUE_NUMBER" --repo "$REPO" --json title,body)
107
- if command -v jq >/dev/null 2>&1; then
108
- ISSUE_TITLE=$(echo "$ISSUE_JSON" | jq -r '.title')
109
- ISSUE_BODY=$(echo "$ISSUE_JSON" | jq -r '.body')
110
- else
111
- ISSUE_TITLE=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print(d["title"])' <<<"$ISSUE_JSON")
112
- ISSUE_BODY=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("body") or "")' <<<"$ISSUE_JSON")
113
- fi
114
-
115
- # 3. Build prompt
116
- PROMPT="Please implement the following GitHub issue.
117
-
118
- Issue #$ISSUE_NUMBER: $ISSUE_TITLE
119
-
120
- $ISSUE_BODY
121
-
122
- Work rules:
123
- - Make the necessary code changes.
124
- - Commit with message: \"autopilot: work for issue #$ISSUE_NUMBER\".
125
- - Push your changes to the remote branch $BRANCH.
126
- - If the issue is a simple file-addition, just do it directly (no extra refactors)."
127
-
128
- # 3.5. Planning step - analyze and plan before implementation
129
- if [[ "$ENABLE_PLANNING_STEP" == "true" ]]; then
130
- echo "[run_opencode_issue.sh] Running planning/analysis step..."
131
-
132
- PLANNING_PROMPT="Please analyze the following GitHub issue and provide a detailed implementation plan. Do NOT make any code changes yet - only analyze and plan.
133
-
134
- Issue #$ISSUE_NUMBER: $ISSUE_TITLE
135
-
136
- $ISSUE_BODY
137
-
138
- Your task:
139
- 1. Analyze the issue requirements thoroughly
140
- 2. Explore the codebase to understand the relevant components
141
- 3. Identify the files that need to be modified or created
142
- 4. Outline the step-by-step approach for implementing this change
143
- 5. Note any potential issues, dependencies, or risks
144
- 6. List any assumptions you're making
145
-
146
- Provide your analysis and plan in a clear, structured format. Focus on WHAT needs to be done and HOW you'll approach it."
147
-
148
- # Run opencode to generate the plan
149
- cd "$WORKTREE"
150
- RAW_OUTPUT=$("$OPENCODE_BIN" run "$PLANNING_PROMPT" 2>&1 || echo "Planning failed")
151
-
152
- # Filter out noise - keep only the final plan output
153
- # Remove tool calls, internal logs, and system messages
154
- PLAN_OUTPUT=$(echo "$RAW_OUTPUT" | grep -vE '^\s*(bash|read|write|edit|grep|glob|question|task|webfetch|google_search|morph-mcp|todowrite|todoread|skill|search)\s' | \
155
- grep -vE '^\s*(Running|Executing|File:|Offset:|Limit:|parameters:|pattern:|path:|include:)' | \
156
- grep -vE '^<[a-z_]+>|^<\/[a-z_]+>|^system:|^user:|^assistant:|^model:' | \
157
- grep -vE '^\s*\$|^\s*\[.*\]|^Using|Called|Invoked' | \
158
- grep -vE '^\s*$|^(command|description|filePath|oldString|newString|questions|header|options):' | \
159
- tail -100)
160
-
161
- # If filtering removed everything, use a fallback
162
- if [[ -z "$PLAN_OUTPUT" ]]; then
163
- PLAN_OUTPUT="Unable to extract a clean plan. The raw output contained excessive tool logs."
164
- fi
165
-
166
- # Post the plan as a comment on the issue
167
- PLAN_COMMENT="## 📋 Implementation Plan
168
-
169
- I've analyzed the issue and codebase. Here's my planned approach:
170
-
171
- \`\`\`
172
- $PLAN_OUTPUT
173
- \`\`\`
174
-
175
- I will now proceed with implementation based on this plan. If you'd like me to adjust my approach, please add a comment with feedback."
176
-
177
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$PLAN_COMMENT" || true
178
- echo "[run_opencode_issue.sh] Planning step completed and posted to issue."
179
- fi
180
-
181
- # 4. Run opencode inside worktree
182
- cd "$WORKTREE"
183
- "$OPENCODE_BIN" run "$PROMPT"
184
-
185
- # 4.5. Comment that implementation is complete
186
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Implementation complete.
187
-
188
- I've finished implementing the solution. Now committing changes and creating PR..." || true
189
-
190
- # 5. Commit any changes OpenCode made
191
- if [[ -n "$(git status --porcelain)" ]]; then
192
- git add -A
193
- git commit -m "autopilot: work for issue #$ISSUE_NUMBER"
194
-
195
- # Comment with commit info
196
- COMMIT_SHA=$(git rev-parse --short HEAD)
197
- CHANGES_COUNT=$(git diff-tree --no-commit-id --name-only -r HEAD | wc -l | xargs)
198
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "📝 Changes committed.
199
-
200
- - Commit: \`$COMMIT_SHA\`
201
- - Files changed: $CHANGES_COUNT
202
- - Branch: \`$BRANCH\`
203
-
204
- Next step: pushing to remote..." || true
205
- else
206
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "ℹ️ No changes were made by the AI agent. This might indicate the issue is already resolved or requires manual intervention." || true
207
- fi
208
-
209
- # 6. Ensure branch is pushed (no-op if already up to date)
210
- git push -u origin "$BRANCH" || true
211
-
212
- # 6.5. Comment after push
213
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "📤 Changes pushed to remote branch \`$BRANCH\`.
214
-
215
- Now creating pull request..." || true
216
-
217
- # 7. Create PR if one doesn't already exist
218
- PR_URL=""
219
- if gh pr view --repo "$REPO" --head "$BRANCH" --json url --jq .url >/dev/null 2>&1; then
220
- PR_URL=$(gh pr view --repo "$REPO" --head "$BRANCH" --json url --jq .url)
221
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "📋 PR already exists: $PR_URL" || true
222
- else
223
- PR_URL=$(gh pr create --repo "$REPO" --title "Autopilot: Issue #$ISSUE_NUMBER" --body "Closes #$ISSUE_NUMBER" --base main --head "$BRANCH")
224
-
225
- # Add short delay after PR creation to allow GitHub to calculate merge state
226
- sleep 10
227
- fi
228
-
229
- # 8. Comment on issue with PR URL (best-effort)
230
- if [[ -n "$PR_URL" ]]; then
231
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "🎉 PR created: $PR_URL
232
-
233
- Autopilot has completed the initial implementation. The PR is now open and ready for review." || true
234
- fi
235
-
236
- # 8.5. Handle merge conflicts if auto-resolve is enabled
237
- if [[ "$AUTO_RESOLVE_CONFLICTS" == "true" ]] && [[ -n "$PR_URL" ]]; then
238
- echo "[run_opencode_issue.sh] Checking for merge conflicts..."
239
-
240
- # Get PR merge status
241
- MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
242
-
243
- if command -v jq >/dev/null 2>&1; then
244
- IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
245
- else
246
- IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
247
- fi
248
-
249
- if [[ "$IS_CONFLICTING" == "true" ]]; then
250
- echo "[run_opencode_issue.sh] PR has merge conflicts. Attempting auto-resolution..."
251
-
252
- RESOLVE_ATTEMPT=0
253
- MAX_ATTEMPTS=$((CONFLICT_RESOLUTION_MAX_ATTEMPTS + 0))
254
-
255
- while [[ $RESOLVE_ATTEMPT -lt $MAX_ATTEMPTS ]]; do
256
- RESOLVE_ATTEMPT=$((RESOLVE_ATTEMPT + 1))
257
- echo "[run_opencode_issue.sh] Conflict resolution attempt $RESOLVE_ATTEMPT/$MAX_ATTEMPTS"
258
-
259
- # Fetch latest main
260
- echo "[run_opencode_issue.sh] Fetching latest main..."
261
- git fetch origin main || true
262
-
263
- # Checkout branch and try to rebase
264
- echo "[run_opencode_issue.sh] Rebasing branch onto main..."
265
- cd "$WORKTREE"
266
- if git rebase origin/main; then
267
- echo "[run_opencode_issue.sh] Rebase successful."
268
-
269
- # Push rebased changes
270
- git push -f origin "$BRANCH" || true
271
-
272
- # Check if conflicts are resolved
273
- MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
274
-
275
- if command -v jq >/dev/null 2>&1; then
276
- IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
277
- else
278
- IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
279
- fi
280
-
281
- if [[ "$IS_CONFLICTING" == "false" ]]; then
282
- echo "[run_opencode_issue.sh] Conflicts resolved successfully."
283
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Autopilot successfully resolved merge conflicts." || true
284
- break
285
- fi
286
- else
287
- # Rebase failed - there are conflicts to resolve
288
- echo "[run_opencode_issue.sh] Rebase encountered conflicts. Attempting resolution with agent..."
289
-
290
- # Abort the rebase to get back to a clean state
291
- git rebase --abort 2>/dev/null || true
292
-
293
- # Try merging main instead (may be easier to resolve)
294
- if git merge origin/main; then
295
- echo "[run_opencode_issue.sh] Merge successful."
296
- git push -f origin "$BRANCH" || true
297
-
298
- # Check if conflicts are resolved
299
- MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
300
-
301
- if command -v jq >/dev/null 2>&1; then
302
- IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
303
- else
304
- IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
305
- fi
306
-
307
- if [[ "$IS_CONFLICTING" == "false" ]]; then
308
- echo "[run_opencode_issue.sh] Conflicts resolved successfully."
309
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Autopilot successfully resolved merge conflicts." || true
310
- break
311
- fi
312
- else
313
- # Merge also has conflicts - abort and try to resolve with agent
314
- git merge --abort 2>/dev/null || true
315
-
316
- echo "[run_opencode_issue.sh] Attempting to resolve conflicts with opencode agent..."
317
-
318
- # Use opencode to resolve conflicts
319
- CONFLICT_PROMPT="This PR has merge conflicts. Please resolve them by examining the conflicted files and making the necessary edits to resolve the conflicts. Follow these steps:
320
- 1. Run 'git status' to see all conflicted files
321
- 2. For each conflicted file, examine the conflict markers (<<<<<<<, =======, >>>>>>>)
322
- 3. Resolve the conflicts by choosing the appropriate code
323
- 4. Stage the resolved files with 'git add <file>'
324
- 5. Commit the resolved files if needed
325
- 6. The goal is to make the branch mergeable with main
326
-
327
- After resolving all conflicts, report the files that were resolved."
328
-
329
- if "$OPENCODE_BIN" run "$CONFLICT_PROMPT"; then
330
- # Check if there are still conflicts
331
- if [[ -z "$(git diff --name-only --diff-filter=U)" ]]; then
332
- echo "[run_opencode_issue.sh] Conflicts resolved by agent."
333
-
334
- # Complete the merge or rebase
335
- git add -A
336
-
337
- # If we were in the middle of a merge, complete it
338
- if [[ -f ".git/MERGE_HEAD" ]]; then
339
- git commit --no-edit || true
340
- fi
341
-
342
- # Push resolved changes
343
- git push -f origin "$BRANCH" || true
344
-
345
- # Check if conflicts are resolved
346
- MERGE_STATUS=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus --jq '{mergeable, mergeStateStatus}' 2>/dev/null || echo '{"mergeable":"UNKNOWN"}')
347
-
348
- if command -v jq >/dev/null 2>&1; then
349
- IS_CONFLICTING=$(echo "$MERGE_STATUS" | jq -r 'if .mergeable == "CONFLICTING" or .mergeStateStatus == "DIRTY" then "true" else "false" end')
350
- else
351
- IS_CONFLICTING=$(python3 -c 'import json,sys; d=json.load(sys.stdin); print("true" if d.get("mergeable") == "CONFLICTING" or d.get("mergeStateStatus") == "DIRTY" else "false")' <<<"$MERGE_STATUS")
352
- fi
353
-
354
- if [[ "$IS_CONFLICTING" == "false" ]]; then
355
- echo "[run_opencode_issue.sh] Conflicts resolved successfully."
356
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "✅ Autopilot successfully resolved merge conflicts." || true
357
- break
358
- fi
359
- fi
360
- fi
361
- fi
362
- fi
363
-
364
- # If we're here, resolution failed and we should try again
365
- echo "[run_opencode_issue.sh] Conflict resolution attempt $RESOLVE_ATTEMPT failed."
366
-
367
- if [[ $RESOLVE_ATTEMPT -ge $MAX_ATTEMPTS ]]; then
368
- echo "[run_opencode_issue.sh] Failed to resolve conflicts after $MAX_ATTEMPTS attempts."
369
-
370
- # Mark issue as blocked
371
- if command -v jq >/dev/null 2>&1; then
372
- LABEL_BLOCKED=$(jq -r '.issueLabels.blocked' < .autopilot/autopilot.json)
373
- else
374
- LABEL_BLOCKED=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["blocked"])')
375
- fi
376
-
377
- BLOCKED_MSG="❌ Autopilot failed to resolve merge conflicts after $MAX_ATTEMPTS attempts.\n\nThe PR will remain open but autopilot has stopped attempting to resolve conflicts.\n\nTo resolve manually:\n1. Checkout the branch: \`git checkout $BRANCH\`\n2. Fetch and rebase main: \`git fetch origin main && git rebase origin/main\`\n3. Resolve conflicts as needed\n4. Push resolved changes: \`git push -f origin $BRANCH\`\n\nOnce resolved, autopilot can attempt to merge the PR."
378
-
379
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BLOCKED_MSG" || true
380
- gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABEL_BLOCKED" || true
381
-
382
- exit 1
383
- fi
384
-
385
- # Wait a bit before next attempt
386
- sleep 5
387
- done
388
- fi
389
- fi
390
-
391
- # 9. Auto-merge if enabled and user is allowed
392
- if [[ "$AUTO_MERGE" == "true" ]] && [[ -n "$PR_URL" ]]; then
393
- # Get authenticated GitHub user
394
- AUTH_USER=$(gh api user --jq .login)
395
-
396
- # Check if user is in allowed list
397
- USER_ALLOWED=false
398
- if [[ -n "$ALLOWED_USERS" ]]; then
399
- while IFS= read -r user; do
400
- if [[ "$AUTH_USER" == "$user" ]]; then
401
- USER_ALLOWED=true
402
- break
403
- fi
404
- done <<< "$ALLOWED_USERS"
405
- fi
406
-
407
- if [[ "$USER_ALLOWED" == "true" ]]; then
408
- echo "[run_opencode_issue.sh] Auto-merge enabled. Waiting for PR checks to pass..."
409
-
410
- # Comment that we're waiting for checks
411
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "⏳ Waiting for PR checks to complete...
412
-
413
- I'm monitoring the CI checks and will auto-merge once they pass. This may take a few minutes." || true
414
-
415
- # Initialize auto-fix attempt counter
416
- FIX_ATTEMPT=0
417
-
418
- # Poll PR checks until they pass or fail
419
- MAX_POLL_ATTEMPTS=60
420
- POLL_INTERVAL=30
421
- POLL_ATTEMPT=0
422
-
423
- while [[ $POLL_ATTEMPT -lt $MAX_POLL_ATTEMPTS ]]; do
424
- POLL_ATTEMPT=$((POLL_ATTEMPT + 1))
425
-
426
- # Get full PR status for debugging and decision making
427
- PR_STATUS_JSON=$(gh pr view --repo "$REPO" --head "$BRANCH" --json mergeable,mergeStateStatus,statusCheckRollup 2>/dev/null || echo '{"mergeable":"UNKNOWN","mergeStateStatus":"UNKNOWN","statusCheckRollup":null}')
428
-
429
- # Log raw API response for debugging
430
- echo "[run_opencode_issue.sh] Debug: Raw PR status = $PR_STATUS_JSON"
431
-
432
- # Parse mergeStateStatus
433
- if command -v jq >/dev/null 2>&1; then
434
- MERGE_STATE_STATUS=$(echo "$PR_STATUS_JSON" | jq -r '.mergeStateStatus')
435
- else
436
- MERGE_STATE_STATUS=$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("mergeStateStatus", "UNKNOWN"))' <<<"$PR_STATUS_JSON")
437
- fi
438
-
439
- # Determine check status based on both statusCheckRollup and mergeStateStatus
440
- # If mergeStateStatus is CLEAN or HAS_HOOKS and statusCheckRollup is null/empty → PASSED
441
- # If mergeStateStatus is UNKNOWN → wait and retry
442
- if [[ "$MERGE_STATE_STATUS" == "UNKNOWN" ]]; then
443
- # Check if there are any CI checks configured
444
- if command -v jq >/dev/null 2>&1; then
445
- SCR_IS_NULL=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup == null')
446
- SCR_LENGTH=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup | length // 0')
447
- else
448
- SCR_VALUE=$(python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("statusCheckRollup", "null"))' <<<"$PR_STATUS_JSON")
449
- SCR_IS_NULL=$([[ "$SCR_VALUE" == "null" ]] && echo "true" || echo "false")
450
- SCR_LENGTH=$(python3 -c 'import json,sys; data=json.load(sys.stdin); scr=data.get("statusCheckRollup"); print(len(scr) if scr else 0)' <<<"$PR_STATUS_JSON")
451
- fi
452
-
453
- # If no checks configured, wait at most 20 seconds for them to appear
454
- if [[ "$SCR_IS_NULL" == "true" ]] || [[ "$SCR_LENGTH" == "0" ]]; then
455
- if [[ $POLL_ATTEMPT -lt 1 ]]; then
456
- CHECK_STATUS="PENDING"
457
- echo "[run_opencode_issue.sh] No CI checks configured, waiting up to 20 seconds for checks to appear..."
458
- sleep 20
459
- continue
460
- else
461
- CHECK_STATUS="PASSED"
462
- echo "[run_opencode_issue.sh] No checks found after waiting 20 seconds, continuing to merge..."
463
- fi
464
- else
465
- CHECK_STATUS="PENDING"
466
- echo "[run_opencode_issue.sh] mergeStateStatus is UNKNOWN, waiting for GitHub to calculate..."
467
- fi
468
- elif [[ "$MERGE_STATE_STATUS" == "CLEAN" || "$MERGE_STATE_STATUS" == "HAS_HOOKS" ]]; then
469
- # Check statusCheckRollup - if null/empty, no checks configured, so PASSED
470
- if command -v jq >/dev/null 2>&1; then
471
- # Debug: log the statusCheckRollup value
472
- SCR_VALUE=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup')
473
- echo "[run_opencode_issue.sh] Debug: statusCheckRollup value = $SCR_VALUE"
474
-
475
- # Check if statusCheckRollup is null or empty array
476
- SCR_IS_NULL=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup == null')
477
- SCR_LENGTH=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup | length // 0')
478
- echo "[run_opencode_issue.sh] Debug: statusCheckRollup is_null=$SCR_IS_NULL, length=$SCR_LENGTH"
479
-
480
- if [[ "$SCR_IS_NULL" == "true" ]] || [[ "$SCR_LENGTH" == "0" ]]; then
481
- CHECK_STATUS="PASSED"
482
- echo "[run_opencode_issue.sh] No CI checks configured (statusCheckRollup is null/empty)"
483
- else
484
- # Has checks - determine status
485
- CHECK_STATUS=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup | map(.conclusion) | if any(. == "FAILURE") then "FAILED" elif any(. == "PENDING") or any(. == "QUEUED") then "PENDING" else "PASSED" end')
486
- echo "[run_opencode_issue.sh] CI checks found, status = $CHECK_STATUS"
487
- fi
488
- else
489
- # Python fallback
490
- SCR_VALUE=$(python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("statusCheckRollup", "null"))' <<<"$PR_STATUS_JSON")
491
- echo "[run_opencode_issue.sh] Debug: statusCheckRollup value = $SCR_VALUE"
492
-
493
- CHECK_STATUS=$(python3 -c 'import json,sys; data=json.load(sys.stdin); scr=data.get("statusCheckRollup"); print("PASSED" if not scr or len(scr)==0 else "FAILED" if any(c.get("conclusion")=="FAILURE" for c in scr) else "PENDING" if any(c.get("conclusion") in ["PENDING","QUEUED"] for c in scr) else "PASSED")' <<<"$PR_STATUS_JSON")
494
-
495
- if [[ "$SCR_VALUE" == "null" ]] || [[ "$SCR_VALUE" == "[]" ]]; then
496
- echo "[run_opencode_issue.sh] No CI checks configured (statusCheckRollup is null/empty)"
497
- fi
498
- fi
499
- elif [[ "$MERGE_STATE_STATUS" == "BLOCKED" ]]; then
500
- # If mergeStateStatus is BLOCKED, check if it's due to failed checks
501
- echo "[run_opencode_issue.sh] mergeStateStatus is BLOCKED, checking if due to failed checks"
502
- if command -v jq >/dev/null 2>&1; then
503
- # Debug: log the statusCheckRollup value
504
- SCR_VALUE=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup')
505
- echo "[run_opencode_issue.sh] Debug: statusCheckRollup value = $SCR_VALUE"
506
-
507
- # Check if statusCheckRollup is null or empty array
508
- SCR_IS_NULL=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup == null')
509
- SCR_LENGTH=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup | length // 0')
510
- echo "[run_opencode_issue.sh] Debug: statusCheckRollup is_null=$SCR_IS_NULL, length=$SCR_LENGTH"
511
-
512
- if [[ "$SCR_IS_NULL" == "true" ]] || [[ "$SCR_LENGTH" == "0" ]]; then
513
- CHECK_STATUS="FAILED"
514
- echo "[run_opencode_issue.sh] No CI checks but mergeStateStatus is BLOCKED - other blocking issue"
515
- else
516
- # Has checks - determine status
517
- CHECK_STATUS=$(echo "$PR_STATUS_JSON" | jq -r '.statusCheckRollup | map(.conclusion) | if any(. == "FAILURE") then "FAILED" elif any(. == "PENDING") or any(. == "QUEUED") then "PENDING" else "PASSED" end')
518
- echo "[run_opencode_issue.sh] CI checks found, status = $CHECK_STATUS"
519
- fi
520
- else
521
- # Python fallback
522
- SCR_VALUE=$(python3 -c 'import json,sys; data=json.load(sys.stdin); print(data.get("statusCheckRollup", "null"))' <<<"$PR_STATUS_JSON")
523
- echo "[run_opencode_issue.sh] Debug: statusCheckRollup value = $SCR_VALUE"
524
-
525
- CHECK_STATUS=$(python3 -c 'import json,sys; data=json.load(sys.stdin); scr=data.get("statusCheckRollup"); print("PASSED" if not scr or len(scr)==0 else "FAILED" if any(c.get("conclusion")=="FAILURE" for c in scr) else "PENDING" if any(c.get("conclusion") in ["PENDING","QUEUED"] for c in scr) else "PASSED")' <<<"$PR_STATUS_JSON")
526
-
527
- if [[ "$SCR_VALUE" == "null" ]] || [[ "$SCR_VALUE" == "[]" ]]; then
528
- echo "[run_opencode_issue.sh] No CI checks but mergeStateStatus is BLOCKED - other blocking issue"
529
- fi
530
- fi
531
- else
532
- # Other states (DRAFT, DIRTY, BEHIND) - mark as FAILED to exit
533
- CHECK_STATUS="FAILED"
534
- fi
535
-
536
- echo "[run_opencode_issue.sh] Poll attempt $POLL_ATTEMPT/$MAX_POLL_ATTEMPTS: Check status = $CHECK_STATUS, mergeStateStatus = $MERGE_STATE_STATUS"
537
-
538
- if [[ "$CHECK_STATUS" == "PASSED" ]]; then
539
- echo "[run_opencode_issue.sh] All checks passed. Proceeding with merge..."
540
- break
541
- elif [[ "$CHECK_STATUS" == "FAILED" ]]; then
542
- echo "[run_opencode_issue.sh] PR checks failed."
543
-
544
- # Check if auto-fix is enabled
545
- if [[ "$AUTO_FIX_CHECKS" == "true" ]] && [[ $FIX_ATTEMPT -lt $AUTO_FIX_CHECKS_MAX_ATTEMPTS ]]; then
546
- FIX_ATTEMPT=$((FIX_ATTEMPT + 1))
547
- echo "[run_opencode_issue.sh] Auto-fix enabled. Attempting to fix failing checks ($FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS)..."
548
-
549
- # Fetch failed check details and logs
550
- CHECK_RUNS_JSON=$(gh api "repos/$REPO/commits/$(git rev-parse HEAD)/check-runs" --jq '.check_runs[] | select(.conclusion == "FAILURE") | {name: .name, conclusion: .conclusion, details_url: .details_url, output: {title: .output.title, summary: .output.summary, text: .output.text}}' 2>/dev/null || echo "[]")
551
-
552
- # Build failure context
553
- FAILURE_CONTEXT="The following CI checks failed:\n"
554
-
555
- # Add failure details to context
556
- if [[ -n "$CHECK_RUNS_JSON" ]]; then
557
- FAILURE_CONTEXT+="$CHECK_RUNS_JSON\n"
558
- fi
559
-
560
- # Generate repair prompt
561
- REPAIR_PROMPT="The PR checks have failed. Please analyze the CI failures and fix the issues.\n\n$FAILURE_CONTEXT\n\nWork rules:\n- Examine the failed checks and identify the root cause\n- Make the necessary code changes to fix the failures\n- Commit with message: \"autopilot: fix CI check failures (attempt $FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS)\"\n- Push your changes to the branch $BRANCH\n- Focus only on fixing the CI failures, do not make unrelated changes"
562
-
563
- echo "[run_opencode_issue.sh] Running opencode agent to fix CI failures..."
564
-
565
- # Run opencode to fix the issue
566
- cd "$WORKTREE"
567
- if "$OPENCODE_BIN" run "$REPAIR_PROMPT"; then
568
- # Commit any changes
569
- if [[ -n "$(git status --porcelain)" ]]; then
570
- git add -A
571
- git commit -m "autopilot: fix CI check failures (attempt $FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS)"
572
- echo "[run_opencode_issue.sh] Committed fixes for CI failures."
573
-
574
- # Push changes
575
- git push origin "$BRANCH"
576
- echo "[run_opencode_issue.sh] Pushed fixes for CI failures."
577
-
578
- # Reset poll counter to re-check CI status
579
- POLL_ATTEMPT=0
580
-
581
- # Wait a moment for checks to start
582
- sleep 10
583
-
584
- # Continue polling
585
- continue
586
- else
587
- echo "[run_opencode_issue.sh] No changes made by opencode agent."
588
- fi
589
- else
590
- echo "[run_opencode_issue.sh] Opencode agent failed to fix CI issues."
591
- fi
592
-
593
- # If we're here, auto-fix failed or made no changes
594
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "⚠️ Autopilot attempted to fix CI failures (attempt $FIX_ATTEMPT/$AUTO_FIX_CHECKS_MAX_ATTEMPTS) but was unable to resolve all issues." || true
595
- fi
596
-
597
- # If auto-fix is disabled or max attempts reached, fail
598
- if [[ "$FIX_ATTEMPT" -ge "$AUTO_FIX_CHECKS_MAX_ATTEMPTS" ]]; then
599
- FAILED_MSG="❌ Autopilot cannot auto-merge PR: checks failed after $AUTO_FIX_CHECKS_MAX_ATTEMPTS auto-fix attempts.\n\nPR will remain open for review."
600
- else
601
- FAILED_MSG="❌ Autopilot cannot auto-merge PR: checks failed (auto-fix disabled).\n\nPR will remain open for review."
602
- fi
603
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$FAILED_MSG" || true
604
- exit 1
605
- fi
606
-
607
- # Wait before next poll
608
- if [[ $POLL_ATTEMPT -lt $MAX_POLL_ATTEMPTS ]]; then
609
- echo "[run_opencode_issue.sh] Waiting ${POLL_INTERVAL}s before next check..."
610
- sleep "$POLL_INTERVAL"
611
- fi
612
- done
613
-
614
- # After polling loop: merge if checks passed
615
- if [[ "$CHECK_STATUS" == "PASSED" ]]; then
616
- echo "[run_opencode_issue.sh] Merging PR using method: $MERGE_METHOD"
617
-
618
- # Build merge command based on method
619
- MERGE_CMD="gh pr merge $PR_URL --delete-branch"
620
- case "$MERGE_METHOD" in
621
- squash)
622
- MERGE_CMD="$MERGE_CMD --squash"
623
- ;;
624
- merge)
625
- MERGE_CMD="$MERGE_CMD --merge"
626
- ;;
627
- rebase)
628
- MERGE_CMD="$MERGE_CMD --rebase"
629
- ;;
630
- *)
631
- echo "[run_opencode_issue.sh] Unknown merge method '$MERGE_METHOD', defaulting to squash"
632
- MERGE_CMD="$MERGE_CMD --squash"
633
- ;;
634
- esac
635
-
636
- # Attempt merge
637
- if eval "$MERGE_CMD"; then
638
- echo "[run_opencode_issue.sh] PR merged successfully!"
639
-
640
- # Update issue with success message and mark as done
641
- SUCCESS_MSG="✅ Autopilot successfully merged PR #${PR_URL##*/} using $MERGE_METHOD method.\n\nThe fix has been merged to the main branch."
642
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$SUCCESS_MSG" || true
643
-
644
- # Get done label and mark issue as done
645
- if command -v jq >/dev/null 2>&1; then
646
- LABEL_DONE=$(jq -r '.issueLabels.done' < .autopilot/autopilot.json)
647
- LABEL_IN_PROGRESS=$(jq -r '.issueLabels.inProgress' < .autopilot/autopilot.json)
648
- else
649
- LABEL_DONE=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["done"])')
650
- LABEL_IN_PROGRESS=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["inProgress"])')
651
- fi
652
-
653
- gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABEL_DONE" --remove-label "$LABEL_IN_PROGRESS" || true
654
- gh issue close "$ISSUE_NUMBER" --repo "$REPO" || true
655
- else
656
- echo "[run_opencode_issue.sh] Failed to merge PR"
657
- MERGE_FAIL_MSG="❌ Autopilot failed to merge PR automatically.\n\nThe PR is ready (checks passed) but the merge operation failed. You may need to merge manually."
658
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$MERGE_FAIL_MSG" || true
659
- fi
660
- elif [[ $POLL_ATTEMPT -ge $MAX_POLL_ATTEMPTS ]]; then
661
- echo "[run_opencode_issue.sh] Timed out waiting for checks to pass"
662
- TIMEOUT_MSG="⏱️ Autopilot timed out waiting for PR checks to complete.\n\nThe PR will remain open for manual review."
663
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$TIMEOUT_MSG" || true
664
- fi
665
- else
666
- BLOCKED_MSG="❌ Autopilot cannot auto-merge PR: authenticated user '$AUTH_USER' is not in the allowedMergeUsers list."
667
- gh issue comment "$ISSUE_NUMBER" --repo "$REPO" --body "$BLOCKED_MSG" || true
668
-
669
- # Mark issue as blocked
670
- if command -v jq >/dev/null 2>&1; then
671
- LABEL_BLOCKED=$(jq -r '.issueLabels.blocked' < .autopilot/autopilot.json)
672
- else
673
- LABEL_BLOCKED=$(python3 -c 'import json; print(json.load(open(".autopilot/autopilot.json"))["issueLabels"]["blocked"])')
674
- fi
675
- gh issue edit "$ISSUE_NUMBER" --repo "$REPO" --add-label "$LABEL_BLOCKED" || true
676
- fi
677
- fi
678
-
679
- echo "[run_opencode_issue.sh] Done: $PR_URL"