diff --git a/dev/dev-agent.sh b/dev/dev-agent.sh index 3a78f53..bdbdb70 100755 --- a/dev/dev-agent.sh +++ b/dev/dev-agent.sh @@ -185,7 +185,11 @@ log "preflight passed" # ============================================================================= # CLAIM ISSUE # ============================================================================= -issue_claim "$ISSUE" +if ! issue_claim "$ISSUE"; then + log "SKIP: failed to claim issue #${ISSUE} (already assigned to another agent)" + echo '{"status":"already_done","reason":"issue was claimed by another agent"}' > "$PREFLIGHT_RESULT" + exit 0 +fi CLAIMED=true # ============================================================================= diff --git a/dev/dev-poll.sh b/dev/dev-poll.sh index 98b8b7d..22ba929 100755 --- a/dev/dev-poll.sh +++ b/dev/dev-poll.sh @@ -307,6 +307,11 @@ memory_guard 2000 # PRIORITY 1: orphaned in-progress issues # ============================================================================= log "checking for in-progress issues" + +# Get current bot identity for assignee checks +BOT_USER=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${API%%/repos*}/user" | jq -r '.login') || BOT_USER="" + ORPHANS_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ "${API}/issues?state=open&labels=in-progress&limit=10&type=issues") @@ -387,7 +392,20 @@ if [ "$ORPHAN_COUNT" -gt 0 ]; then log "issue #${ISSUE_NUM} has open PR #${HAS_PR} (CI: ${CI_STATE}, waiting)" fi else - log "recovering orphaned issue #${ISSUE_NUM} (no PR found)" + # Check assignee before adopting orphaned issue + ISSUE_JSON=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${API}/issues/${ISSUE_NUM}") || true + ASSIGNEE=$(echo "$ISSUE_JSON" | jq -r '.assignee.login // ""') || true + + if [ -n "$ASSIGNEE" ] && [ "$ASSIGNEE" != "$BOT_USER" ]; then + log "issue #${ISSUE_NUM} assigned to ${ASSIGNEE} — skipping (not orphaned)" + # Remove in-progress label since this agent isn't working on it + curl -sf -X DELETE -H "Authorization: token ${FORGE_TOKEN}" \ + "${API}/issues/${ISSUE_NUM}/labels/in-progress" >/dev/null 2>&1 || true + exit 0 + fi + + log "recovering orphaned issue #${ISSUE_NUM} (no PR found, assigned to ${BOT_USER:-unassigned})" nohup "${SCRIPT_DIR}/dev-agent.sh" "$ISSUE_NUM" >> "$LOGFILE" 2>&1 & log "started dev-agent PID $! for issue #${ISSUE_NUM} (recovery)" exit 0 diff --git a/lib/issue-lifecycle.sh b/lib/issue-lifecycle.sh index df6a0ae..19c422d 100644 --- a/lib/issue-lifecycle.sh +++ b/lib/issue-lifecycle.sh @@ -81,11 +81,35 @@ _ilc_in_progress_id() { _ilc_ensure_label_id _ILC_IN_PROGRESS_ID "in-progress" _ilc_blocked_id() { _ilc_ensure_label_id _ILC_BLOCKED_ID "blocked" "#e11d48"; } # --------------------------------------------------------------------------- -# issue_claim — add "in-progress" label, remove "backlog" label. +# issue_claim — assign issue to bot, add "in-progress" label, remove "backlog". # Args: issue_number +# Returns: 0 on success, 1 if already assigned to another agent # --------------------------------------------------------------------------- issue_claim() { local issue="$1" + + # Get current bot identity + local me + me=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_URL}/api/v1/user" | jq -r '.login') || return 1 + + # Check current assignee + local current + current=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ + "${FORGE_API}/issues/${issue}" | jq -r '.assignee.login // ""') || return 1 + + if [ -n "$current" ] && [ "$current" != "$me" ]; then + _ilc_log "issue #${issue} already assigned to ${current} — skipping" + return 1 + fi + + # Assign to self (Forgejo rejects if already assigned differently) + curl -sf -X PATCH \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/issues/${issue}" \ + -d "{\"assignees\":[\"${me}\"]}" >/dev/null 2>&1 || return 1 + local ip_id bl_id ip_id=$(_ilc_in_progress_id) bl_id=$(_ilc_backlog_id) @@ -102,14 +126,23 @@ issue_claim() { "${FORGE_API}/issues/${issue}/labels/${bl_id}" >/dev/null 2>&1 || true fi _ilc_log "claimed issue #${issue}" + return 0 } # --------------------------------------------------------------------------- -# issue_release — remove "in-progress" label, add "backlog" label. +# issue_release — remove "in-progress" label, add "backlog" label, clear assignee. # Args: issue_number # --------------------------------------------------------------------------- issue_release() { local issue="$1" + + # Clear assignee + curl -sf -X PATCH \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/issues/${issue}" \ + -d '{"assignees":[]}' >/dev/null 2>&1 || true + local ip_id bl_id ip_id=$(_ilc_in_progress_id) bl_id=$(_ilc_backlog_id) @@ -184,11 +217,19 @@ issue_block() { } # --------------------------------------------------------------------------- -# issue_close — PATCH state to closed. +# issue_close — clear assignee, PATCH state to closed. # Args: issue_number # --------------------------------------------------------------------------- issue_close() { local issue="$1" + + # Clear assignee before closing + curl -sf -X PATCH \ + -H "Authorization: token ${FORGE_TOKEN}" \ + -H "Content-Type: application/json" \ + "${FORGE_API}/issues/${issue}" \ + -d '{"assignees":[]}' >/dev/null 2>&1 || true + curl -sf -X PATCH \ -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \