|
#!/usr/bin/env bash |
|
set -euo pipefail |
|
|
|
############################################################################### |
|
# issue-orchestrator.sh |
|
# |
|
# Orchestrates end-to-end remediation of security and code quality issues |
|
# created by alert-planner.sh. Picks unassigned issues labeled "security" or |
|
# "code-quality", implements the fix, creates a PR, reviews it, addresses |
|
# feedback, and leaves the PR ready for human review. |
|
# |
|
# Usage: ./scripts/issue-orchestrator.sh [--dry-run] [--count N] |
|
# |
|
# Options: |
|
# --dry-run Preview what would happen without making changes |
|
# --count N Process up to N issues in a single run (default: 1) |
|
# |
|
# Prerequisites: gh (authenticated), copilot CLI, git |
|
############################################################################### |
|
|
|
SECURITY_LABEL="security" |
|
CODE_QUALITY_LABEL="code-quality" |
|
DRY_RUN=false |
|
MAX_COUNT=1 |
|
COPILOT_FLAGS=(--allow-all-tools --allow-all-urls) |
|
|
|
# Shared instruction appended to all prompts that involve GitHub operations. |
|
# Ensures copilot CLI agents use gh instead of raw REST API calls. |
|
GH_INSTRUCTION="IMPORTANT: Use the gh CLI for ALL GitHub operations — reading issues, PRs, diffs, review comments, CI check status, and posting reviews. Never call the GitHub REST API directly. The gh CLI is already authenticated." |
|
|
|
while [[ $# -gt 0 ]]; do |
|
case "$1" in |
|
--dry-run) DRY_RUN=true; shift ;; |
|
--count) |
|
if [[ -z "${2:-}" || "$2" =~ ^- ]]; then |
|
echo "❌ --count requires a positive integer argument" >&2; exit 1 |
|
fi |
|
MAX_COUNT="$2"; shift 2 ;; |
|
*) echo "❌ Unknown argument: $1" >&2; echo "Usage: ./scripts/issue-orchestrator.sh [--dry-run] [--count N]" >&2; exit 1 ;; |
|
esac |
|
done |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo "🔍 Dry-run mode — no changes will be made" |
|
fi |
|
if [[ "$MAX_COUNT" -gt 1 ]]; then |
|
echo "🔄 Batch mode — processing up to $MAX_COUNT issues" |
|
fi |
|
|
|
# Track state for cleanup on failure (reset per iteration in batch mode) |
|
ISSUE_NUMBER="" |
|
BRANCH_NAME="" |
|
CURRENT_USER="" |
|
ORIGINAL_BRANCH="" |
|
CODEX_SESSION_ID="" |
|
CODEX_SESSION_FILE="" |
|
WORKTREE_DIR="" |
|
REPO_ROOT="" |
|
|
|
# ── Cleanup on failure ────────────────────────────────────────────────────── |
|
|
|
cleanup_on_failure() { |
|
echo "" |
|
echo "⚠️ Orchestration failed — cleaning up..." >&2 |
|
|
|
if [[ -n "$ISSUE_NUMBER" && -n "$CURRENT_USER" ]]; then |
|
echo " Unassigning issue #$ISSUE_NUMBER..." >&2 |
|
gh issue edit "$ISSUE_NUMBER" --repo "$REPO_OWNER/$REPO_NAME" \ |
|
--remove-assignee "$CURRENT_USER" 2>/dev/null || true |
|
fi |
|
|
|
# Remove worktree and branch |
|
if [[ -n "$WORKTREE_DIR" && -d "$WORKTREE_DIR" ]]; then |
|
echo " Removing worktree $WORKTREE_DIR..." >&2 |
|
cd "$REPO_ROOT" 2>/dev/null || true |
|
git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || true |
|
fi |
|
if [[ -n "$BRANCH_NAME" ]]; then |
|
git branch -D "$BRANCH_NAME" 2>/dev/null || true |
|
fi |
|
|
|
if [[ -n "$CODEX_SESSION_FILE" && -f "$CODEX_SESSION_FILE" ]]; then |
|
rm -f "$CODEX_SESSION_FILE" |
|
fi |
|
|
|
echo " Cleanup complete." >&2 |
|
exit 1 |
|
} |
|
|
|
trap cleanup_on_failure ERR INT TERM |
|
|
|
# ── Prerequisites ──────────────────────────────────────────────────────────── |
|
|
|
check_prerequisites() { |
|
local missing=() |
|
command -v gh >/dev/null 2>&1 || missing+=("gh") |
|
command -v copilot >/dev/null 2>&1 || missing+=("copilot") |
|
command -v git >/dev/null 2>&1 || missing+=("git") |
|
command -v jq >/dev/null 2>&1 || missing+=("jq") |
|
|
|
if [[ ${#missing[@]} -gt 0 ]]; then |
|
echo "❌ Missing required tools: ${missing[*]}" >&2 |
|
exit 1 |
|
fi |
|
|
|
if ! gh auth status >/dev/null 2>&1; then |
|
echo "❌ gh CLI is not authenticated. Run 'gh auth login' first." >&2 |
|
exit 1 |
|
fi |
|
|
|
if ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then |
|
echo "❌ Not inside a git repository." >&2 |
|
exit 1 |
|
fi |
|
} |
|
|
|
# ── Repo detection ─────────────────────────────────────────────────────────── |
|
|
|
detect_repo() { |
|
local remote_url |
|
remote_url=$(git remote get-url origin 2>/dev/null) |
|
REPO_FULL=$(echo "$remote_url" | sed -E 's#.*github\.com[:/]##; s/\.git$//') |
|
REPO_OWNER=$(echo "$REPO_FULL" | cut -d/ -f1) |
|
REPO_NAME=$(echo "$REPO_FULL" | cut -d/ -f2) |
|
|
|
if [[ -z "$REPO_OWNER" || -z "$REPO_NAME" ]]; then |
|
echo "❌ Could not detect repo owner/name from git remote." >&2 |
|
exit 1 |
|
fi |
|
|
|
CURRENT_USER=$(gh api /user --jq '.login' 2>/dev/null) |
|
REPO_ROOT=$(git rev-parse --show-toplevel) |
|
DEFAULT_BRANCH=$(gh repo view "$REPO_OWNER/$REPO_NAME" --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || echo "main") |
|
|
|
echo "📦 Repository: $REPO_OWNER/$REPO_NAME" |
|
echo "👤 User: $CURRENT_USER" |
|
echo "🌿 Default branch: $DEFAULT_BRANCH" |
|
} |
|
|
|
# ── Select unassigned security issue ──────────────────────────────────────── |
|
|
|
select_issue() { |
|
echo "🔎 Looking for unassigned security & code quality issues..." |
|
|
|
# Fetch issues from both labels and merge |
|
local security_issues code_quality_issues |
|
security_issues=$(gh issue list \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--label "$SECURITY_LABEL" \ |
|
--state open \ |
|
--search "no:assignee" \ |
|
--json number,title,body,url \ |
|
--jq '[.[] | select(.title | startswith("[Security]") or startswith("[Code Quality]"))]' 2>/dev/null || echo '[]') |
|
|
|
code_quality_issues=$(gh issue list \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--label "$CODE_QUALITY_LABEL" \ |
|
--state open \ |
|
--search "no:assignee" \ |
|
--json number,title,body,url \ |
|
--jq '[.[] | select(.title | startswith("[Security]") or startswith("[Code Quality]"))]' 2>/dev/null || echo '[]') |
|
|
|
# Merge, deduplicate by number, sort |
|
local issues |
|
issues=$(echo "$security_issues $code_quality_issues" | jq -s 'add | unique_by(.number) | sort_by(.number)' 2>/dev/null || echo '[]') |
|
|
|
local count |
|
count=$(echo "$issues" | jq 'length' 2>/dev/null || echo 0) |
|
|
|
if [[ "$count" -eq 0 ]]; then |
|
echo "✅ No unassigned issues found. Nothing to do!" |
|
return 1 |
|
fi |
|
|
|
# Pick the first unassigned issue |
|
local candidate |
|
candidate=$(echo "$issues" | jq '.[0]') |
|
|
|
ISSUE_NUMBER=$(echo "$candidate" | jq -r '.number') |
|
ISSUE_TITLE=$(echo "$candidate" | jq -r '.title') |
|
ISSUE_BODY=$(echo "$candidate" | jq -r '.body') |
|
ISSUE_URL=$(echo "$candidate" | jq -r '.url') |
|
echo " 🎯 Selected issue #$ISSUE_NUMBER: $ISSUE_TITLE" |
|
} |
|
|
|
# ── Step 1: Assign issue ──────────────────────────────────────────────────── |
|
|
|
assign_issue() { |
|
echo "" |
|
echo "── Step 1: Assign issue ───────────────────────────────────" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would assign #$ISSUE_NUMBER to $CURRENT_USER" |
|
return 0 |
|
fi |
|
|
|
gh issue edit "$ISSUE_NUMBER" \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--add-assignee "$CURRENT_USER" |
|
|
|
# Verify we won the race — check the issue is assigned to us |
|
local actual_assignee |
|
actual_assignee=$(gh issue view "$ISSUE_NUMBER" \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--json assignees --jq '.assignees[0].login' 2>/dev/null || echo "") |
|
if [[ "$actual_assignee" != "$CURRENT_USER" ]]; then |
|
echo " ⚠️ Issue #$ISSUE_NUMBER was claimed by $actual_assignee — skipping" >&2 |
|
ISSUE_NUMBER="" |
|
exit 1 |
|
fi |
|
|
|
echo " ✅ Assigned #$ISSUE_NUMBER to $CURRENT_USER" |
|
} |
|
|
|
# ── Step 2: Create worktree with fix branch ───────────────────────────────── |
|
|
|
create_worktree() { |
|
echo "" |
|
echo "── Step 2: Create worktree ────────────────────────────────" |
|
|
|
BRANCH_NAME="fix/security-${ISSUE_NUMBER}" |
|
WORKTREE_DIR="${REPO_ROOT}/.worktrees/${BRANCH_NAME}" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would create worktree at $WORKTREE_DIR on branch $BRANCH_NAME" |
|
return 0 |
|
fi |
|
|
|
git fetch origin "$DEFAULT_BRANCH" --quiet |
|
mkdir -p "$(dirname "$WORKTREE_DIR")" |
|
|
|
# Clean up stale branch/worktree from a previous failed run |
|
if git worktree list --porcelain | grep -q "$WORKTREE_DIR"; then |
|
echo " 🧹 Removing stale worktree from previous run..." |
|
git worktree remove --force "$WORKTREE_DIR" 2>/dev/null || true |
|
fi |
|
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then |
|
echo " 🧹 Removing stale branch from previous run..." |
|
git branch -D "$BRANCH_NAME" 2>/dev/null || true |
|
fi |
|
|
|
git worktree add -b "$BRANCH_NAME" "$WORKTREE_DIR" "origin/$DEFAULT_BRANCH" |
|
cd "$WORKTREE_DIR" |
|
|
|
echo " ✅ Created worktree at $WORKTREE_DIR" |
|
echo " 🌿 Branch: $BRANCH_NAME" |
|
} |
|
|
|
# ── Step 3: Implement fix (gpt-5.3-codex) ─────────────────────────────────── |
|
|
|
implement_fix() { |
|
echo "" |
|
echo "── Step 3: Implement fix (gpt-5.3-codex) ─────────────────" |
|
|
|
local task="Read GitHub issue #${ISSUE_NUMBER} (use: gh issue view ${ISSUE_NUMBER} --repo ${REPO_OWNER}/${REPO_NAME}) and implement the fix described in the remediation plan. Make the minimal code changes needed. Do not modify unrelated files. |
|
|
|
${GH_INSTRUCTION}" |
|
local prompt="/ralph-wiggum:ralph-loop \"${task}\" --completion-promise \"DONE\" --max-iterations 10" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would run: copilot -sp \"$prompt\" --model gpt-5.3-codex ${COPILOT_FLAGS[*]} --share copilot-session-fix-${ISSUE_NUMBER}.md" |
|
return 0 |
|
fi |
|
|
|
echo " 🤖 Invoking gpt-5.3-codex to implement the fix..." |
|
echo " This may take several minutes..." |
|
|
|
CODEX_SESSION_FILE="copilot-session-fix-${ISSUE_NUMBER}.md" |
|
|
|
copilot -sp "$prompt" --model gpt-5.3-codex "${COPILOT_FLAGS[@]}" --share "$CODEX_SESSION_FILE" || { |
|
echo " ❌ Fix implementation failed" >&2 |
|
return 1 |
|
} |
|
|
|
# Extract session ID from the exported markdown (format: > **Session ID:** `<uuid>`) |
|
if [[ -f "$CODEX_SESSION_FILE" ]]; then |
|
CODEX_SESSION_ID=$(sed -n 's/.*Session ID:\*\* `\([^`]*\)`.*/\1/p' "$CODEX_SESSION_FILE" 2>/dev/null || true) |
|
if [[ -n "$CODEX_SESSION_ID" ]]; then |
|
echo " 📎 Captured session ID: $CODEX_SESSION_ID" |
|
else |
|
echo " ⚠️ Could not extract session ID from $CODEX_SESSION_FILE" >&2 |
|
fi |
|
fi |
|
|
|
echo " ✅ Fix implementation complete" |
|
|
|
# Verify that codex actually changed something |
|
if [[ -z "$(git status --porcelain)" ]]; then |
|
echo " ❌ No files were modified by the fix implementation" >&2 |
|
return 1 |
|
fi |
|
} |
|
|
|
# ── Append validation summary to PR description ──────────────────────────── |
|
|
|
append_pr_summary() { |
|
local phase="$1" # e.g. "Fix Implementation" or "Review Feedback" |
|
|
|
echo "" |
|
echo "── Updating PR description ($phase) ────────────────────────" |
|
|
|
local prompt |
|
prompt="Summarize what you just did in this repository for the PR description. Include: |
|
1. What files were changed and why |
|
2. What validation or testing steps you performed (e.g. build, lint, tests) |
|
3. Any risks or caveats the reviewer should know about |
|
Be concise — use bullet points. Output only the summary, no preamble." |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would ask codex to summarize and append to PR #$PR_NUMBER" |
|
return 0 |
|
fi |
|
|
|
local summary |
|
local resume_flag=() |
|
if [[ -n "$CODEX_SESSION_ID" ]]; then |
|
resume_flag=(--resume "$CODEX_SESSION_ID") |
|
fi |
|
|
|
summary=$(copilot -sp "$prompt" --model gpt-5.3-codex "${COPILOT_FLAGS[@]}" "${resume_flag[@]}" 2>/dev/null) || { |
|
echo " ⚠️ Could not generate summary — skipping" >&2 |
|
return 0 |
|
} |
|
|
|
local current_body |
|
current_body=$(gh pr view "$PR_NUMBER" --repo "$REPO_OWNER/$REPO_NAME" --json body --jq '.body' 2>/dev/null || echo "") |
|
|
|
gh pr edit "$PR_NUMBER" \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--body "${current_body} |
|
|
|
## 🤖 ${phase} — AI Summary |
|
|
|
${summary}" 2>/dev/null || true |
|
|
|
echo " ✅ PR description updated with $phase summary" |
|
} |
|
|
|
# ── Step 4: Commit, push, create PR (claude-opus-4.6) ─────────────────────── |
|
|
|
commit_push_pr() { |
|
echo "" |
|
echo "── Step 4: Commit, push, create PR (claude-opus-4.6) ─────" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would run: copilot -sp \"/commit-commands:commit-push-pr\" --model claude-opus-4.6 ${COPILOT_FLAGS[*]}" |
|
return 0 |
|
fi |
|
|
|
echo " 🤖 Invoking claude-opus-4.6 to commit, push, and create PR..." |
|
|
|
copilot -sp "/commit-commands:commit-push-pr" --model claude-opus-4.6 "${COPILOT_FLAGS[@]}" || { |
|
echo " ❌ Commit/push/PR creation failed" >&2 |
|
return 1 |
|
} |
|
|
|
echo " ✅ PR created" |
|
} |
|
|
|
# ── Step 5: Capture PR number ──────────────────────────────────────────────── |
|
|
|
capture_pr_number() { |
|
echo "" |
|
echo "── Step 5: Capture PR number ──────────────────────────────" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
PR_NUMBER="(dry-run)" |
|
PR_URL="(dry-run)" |
|
echo " [DRY RUN] Would capture PR number from branch $BRANCH_NAME" |
|
return 0 |
|
fi |
|
|
|
PR_NUMBER=$(gh pr list --repo "$REPO_OWNER/$REPO_NAME" --head "$BRANCH_NAME" --state open --json number --jq '.[0].number' 2>/dev/null) || true |
|
|
|
if [[ -z "$PR_NUMBER" || "$PR_NUMBER" == "null" ]]; then |
|
echo " ❌ Could not find PR for branch $BRANCH_NAME" >&2 |
|
return 1 |
|
fi |
|
|
|
PR_URL=$(gh pr view "$PR_NUMBER" --repo "$REPO_OWNER/$REPO_NAME" --json url --jq '.url' 2>/dev/null) |
|
|
|
# Link PR to issue via "Closes #N" in PR body — this creates the native |
|
# "Development" link so the issue auto-closes when the PR is merged |
|
local current_body |
|
current_body=$(gh pr view "$PR_NUMBER" --repo "$REPO_OWNER/$REPO_NAME" --json body --jq '.body' 2>/dev/null || echo "") |
|
gh pr edit "$PR_NUMBER" \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--body "${current_body} |
|
|
|
--- |
|
Closes #${ISSUE_NUMBER}" 2>/dev/null || true |
|
|
|
echo " ✅ PR #$PR_NUMBER: $PR_URL" |
|
echo " 🔗 Linked to issue #$ISSUE_NUMBER (will auto-close on merge)" |
|
} |
|
|
|
# ── Step 6: Validate CI checks (gpt-5.3-codex) ────────────────────────────── |
|
|
|
validate_ci_checks() { |
|
echo "" |
|
echo "── Step 6: Validate CI checks (gpt-5.3-codex) ─────────────" |
|
|
|
local prompt |
|
prompt="You are validating a security fix on pull request #${PR_NUMBER} in repo ${REPO_OWNER}/${REPO_NAME}. |
|
|
|
1. Check CI status: gh pr checks ${PR_NUMBER} --repo ${REPO_OWNER}/${REPO_NAME} |
|
2. Wait for CI checks to complete — poll every 30 seconds for up to 10 minutes |
|
3. If any check fails: |
|
a. Read the failed check's logs (use: gh run view <run-id> --repo ${REPO_OWNER}/${REPO_NAME} --log-failed) |
|
b. If the failure is caused by your changes, fix the code, commit, and push |
|
c. If the failure is a flaky/unrelated test, note it but do not modify unrelated code |
|
4. After fixing, wait for the new CI run to pass |
|
5. Summarize the CI validation result: which checks passed, which failed, and what you fixed |
|
|
|
Do not modify files unrelated to the fix. If CI passes on the first try, just confirm it. |
|
|
|
${GH_INSTRUCTION}" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would validate CI checks on PR #$PR_NUMBER via gpt-5.3-codex" |
|
return 0 |
|
fi |
|
|
|
echo " 🤖 Invoking gpt-5.3-codex to validate CI checks..." |
|
|
|
local resume_flag=() |
|
if [[ -n "$CODEX_SESSION_ID" ]]; then |
|
resume_flag=(--resume "$CODEX_SESSION_ID") |
|
echo " 📎 Resuming session: $CODEX_SESSION_ID" |
|
fi |
|
|
|
copilot -sp "$prompt" --model gpt-5.3-codex "${COPILOT_FLAGS[@]}" "${resume_flag[@]}" || { |
|
echo " ⚠️ CI validation failed — PR still available for manual review" >&2 |
|
return 0 |
|
} |
|
|
|
echo " ✅ CI checks validated" |
|
} |
|
|
|
# ── Step 7: PR review (claude-opus-4.6) ────────────────────────────────────── |
|
|
|
run_pr_review() { |
|
echo "" |
|
echo "── Step 7: PR review (claude-opus-4.6) ────────────────────" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
REVIEW_OUTPUT="[DRY RUN] Review would be generated" |
|
echo " [DRY RUN] Would run: copilot -sp \"/pr-review-toolkit:review-pr\" --model claude-opus-4.6 ${COPILOT_FLAGS[*]}" |
|
return 0 |
|
fi |
|
|
|
echo " 🤖 Invoking claude-opus-4.6 to review PR #$PR_NUMBER..." |
|
|
|
REVIEW_OUTPUT=$(copilot -sp "/pr-review-toolkit:review-pr" --model claude-opus-4.6 "${COPILOT_FLAGS[@]}" 2>/dev/null) || { |
|
echo " ⚠️ PR review failed — continuing without review" >&2 |
|
REVIEW_OUTPUT="" |
|
return 0 |
|
} |
|
|
|
echo " ✅ PR review complete" |
|
} |
|
|
|
# ── Step 8: Post review to PR (claude-sonnet-4.6) ─────────────────────────── |
|
|
|
post_review() { |
|
echo "" |
|
echo "── Step 8: Post review to PR (claude-sonnet-4.6) ──────────" |
|
|
|
if [[ -z "$REVIEW_OUTPUT" ]]; then |
|
echo " ⏭️ No review output to post — skipping" |
|
return 0 |
|
fi |
|
|
|
local prompt |
|
prompt="Add the following code review as review comments on pull request #${PR_NUMBER} in repo ${REPO_OWNER}/${REPO_NAME}. Use inline comments on specific files/lines where applicable. |
|
|
|
Review: |
|
${REVIEW_OUTPUT} |
|
|
|
${GH_INSTRUCTION}" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would post review to PR #$PR_NUMBER via claude-sonnet-4.6" |
|
return 0 |
|
fi |
|
|
|
echo " 🤖 Invoking claude-sonnet-4.6 to post review comments..." |
|
|
|
copilot -sp "$prompt" --model claude-sonnet-4.6 "${COPILOT_FLAGS[@]}" || { |
|
echo " ⚠️ Posting review failed — continuing" >&2 |
|
return 0 |
|
} |
|
|
|
echo " ✅ Review posted to PR #$PR_NUMBER" |
|
} |
|
|
|
# ── Step 9: Address review feedback (gpt-5.3-codex) ───────────────────────── |
|
|
|
address_feedback() { |
|
echo "" |
|
echo "── Step 9: Address review feedback (gpt-5.3-codex) ────────" |
|
|
|
local prompt |
|
prompt="View the review comments on pull request #${PR_NUMBER} in repo ${REPO_OWNER}/${REPO_NAME} (use: gh api repos/${REPO_OWNER}/${REPO_NAME}/pulls/${PR_NUMBER}/comments). For each review comment: |
|
1. Evaluate if the feedback is valid and actionable |
|
2. If valid, implement the suggested change |
|
3. Reply to the comment explaining what you did (use: gh api repos/${REPO_OWNER}/${REPO_NAME}/pulls/comments/{comment_id}/replies -f body='...') |
|
4. Resolve the comment |
|
When done with all comments, commit and push your changes. |
|
|
|
${GH_INSTRUCTION}" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would address PR #$PR_NUMBER feedback via gpt-5.3-codex --resume $CODEX_SESSION_ID" |
|
return 0 |
|
fi |
|
|
|
echo " 🤖 Invoking gpt-5.3-codex to address review feedback..." |
|
|
|
# Resume the original codex session so the model has full context of the fix |
|
local resume_flag=() |
|
if [[ -n "$CODEX_SESSION_ID" ]]; then |
|
resume_flag=(--resume "$CODEX_SESSION_ID") |
|
echo " 📎 Resuming session: $CODEX_SESSION_ID" |
|
fi |
|
|
|
copilot -sp "$prompt" --model gpt-5.3-codex "${COPILOT_FLAGS[@]}" "${resume_flag[@]}" || { |
|
echo " ⚠️ Addressing feedback failed — PR still available for manual review" >&2 |
|
return 0 |
|
} |
|
|
|
echo " ✅ Review feedback addressed" |
|
} |
|
|
|
# ── Step 10: AI Judge (claude-sonnet-4.6) ──────────────────────────────────── |
|
|
|
ai_judge() { |
|
echo "" |
|
echo "── Step 10: AI Judge (claude-sonnet-4.6) ───────────────────" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would invoke claude-sonnet-4.6 to judge the fix" |
|
return 0 |
|
fi |
|
|
|
local prompt="You are acting as an impartial judge reviewing a security fix. |
|
|
|
Here is the original security issue: |
|
- Issue #${ISSUE_NUMBER}: ${ISSUE_TITLE} |
|
- Issue URL: ${ISSUE_URL} |
|
|
|
Here is the PR that addresses it: |
|
- PR #${PR_NUMBER}: ${PR_URL} |
|
|
|
Please do the following: |
|
1. Read the original issue body (use: gh issue view ${ISSUE_NUMBER} --repo ${REPO_OWNER}/${REPO_NAME}) |
|
2. Read the PR diff (use: gh pr diff ${PR_NUMBER} --repo ${REPO_OWNER}/${REPO_NAME}) |
|
3. Evaluate whether the fix correctly and completely addresses the security issue |
|
4. Leave a single PR review comment (use: gh pr review ${PR_NUMBER} --repo ${REPO_OWNER}/${REPO_NAME} --comment --body '...') |
|
|
|
Your review comment must include: |
|
- ✅ or ❌ verdict (does the fix address the issue?) |
|
- A brief rationale (2-3 sentences max) |
|
- Any remaining concerns or risks |
|
- A confidence level (High/Medium/Low) |
|
|
|
Keep it concise — this is for a human to make a quick approve/reject decision. |
|
|
|
${GH_INSTRUCTION}" |
|
|
|
echo " ⚖️ Invoking claude-sonnet-4.6 as judge..." |
|
copilot -sp "$prompt" --model claude-sonnet-4.6 "${COPILOT_FLAGS[@]}" || { |
|
echo " ⚠️ Judge review failed — PR still available for manual review" >&2 |
|
return 0 |
|
} |
|
|
|
echo " ✅ AI judge review posted" |
|
} |
|
|
|
# ── Step 11: Finalize ─────────────────────────────────────────────────────── |
|
|
|
finalize() { |
|
echo "" |
|
echo "── Step 11: Finalize ──────────────────────────────────────" |
|
|
|
if [[ "$DRY_RUN" == true ]]; then |
|
echo " [DRY RUN] Would add PR link to issue" |
|
echo " 🏷️ Issue: #$ISSUE_NUMBER — $ISSUE_TITLE" |
|
return 0 |
|
fi |
|
|
|
# Comment on issue with PR link |
|
gh issue comment "$ISSUE_NUMBER" \ |
|
--repo "$REPO_OWNER/$REPO_NAME" \ |
|
--body "🔧 Automated fix submitted in PR #${PR_NUMBER}: ${PR_URL} |
|
|
|
This PR has been reviewed by AI and feedback has been addressed. Ready for human review and merge." 2>/dev/null || true |
|
|
|
# Return to repo root and remove worktree |
|
cd "$REPO_ROOT" |
|
git worktree remove "$WORKTREE_DIR" 2>/dev/null || true |
|
|
|
# Clean up session file |
|
if [[ -n "$CODEX_SESSION_FILE" && -f "$CODEX_SESSION_FILE" ]]; then |
|
rm -f "$CODEX_SESSION_FILE" |
|
fi |
|
|
|
echo " ✅ Issue #$ISSUE_NUMBER — PR #$PR_NUMBER ready for human review" |
|
} |
|
|
|
# ── Main ───────────────────────────────────────────────────────────────────── |
|
|
|
main() { |
|
echo "" |
|
echo "🔧 Issue Orchestrator — Automated Remediation Pipeline" |
|
echo "═══════════════════════════════════════════════════════════" |
|
echo "" |
|
|
|
check_prerequisites |
|
detect_repo |
|
|
|
local processed=0 |
|
while [[ "$processed" -lt "$MAX_COUNT" ]]; do |
|
# Reset per-issue state for each iteration |
|
ISSUE_NUMBER="" |
|
ISSUE_TITLE="" |
|
ISSUE_BODY="" |
|
ISSUE_URL="" |
|
BRANCH_NAME="" |
|
WORKTREE_DIR="" |
|
CODEX_SESSION_ID="" |
|
CODEX_SESSION_FILE="" |
|
PR_NUMBER="" |
|
PR_URL="" |
|
REVIEW_OUTPUT="" |
|
|
|
if ! select_issue; then |
|
break |
|
fi |
|
|
|
assign_issue |
|
create_worktree |
|
implement_fix |
|
commit_push_pr |
|
capture_pr_number |
|
validate_ci_checks |
|
append_pr_summary "Fix Implementation" |
|
run_pr_review |
|
post_review |
|
address_feedback |
|
append_pr_summary "Review Feedback Addressed" |
|
ai_judge |
|
finalize |
|
|
|
processed=$((processed + 1)) |
|
|
|
if [[ "$MAX_COUNT" -gt 1 ]]; then |
|
echo "" |
|
echo " 📊 Progress: $processed / $MAX_COUNT" |
|
fi |
|
done |
|
|
|
echo "" |
|
echo "═══════════════════════════════════════════════════════════" |
|
echo "✅ Orchestration complete — processed $processed issue(s)" |
|
echo "═══════════════════════════════════════════════════════════" |
|
} |
|
|
|
main |