diff --git a/.agents/scripts/commands/pulse.md b/.agents/scripts/commands/pulse.md index abad0107ea..7ef7056100 100644 --- a/.agents/scripts/commands/pulse.md +++ b/.agents/scripts/commands/pulse.md @@ -60,13 +60,20 @@ Check external contributor gate before ANY merge (see Pre-merge checks below). For each unassigned, non-blocked issue with no open PR and no active worker: ```bash -# Dedup guard (MANDATORY) +# Dedup guard (MANDATORY — all three checks required) source ~/.aidevops/agents/scripts/pulse-wrapper.sh +RUNNER_USER=$(gh api user --jq '.login' 2>/dev/null || whoami) + +# 1. Local process dedup (same machine only) if has_worker_for_repo_issue NUMBER SLUG; then continue; fi if ~/.aidevops/agents/scripts/dispatch-dedup-helper.sh is-duplicate "Issue #NUMBER: TITLE"; then continue; fi +# 2. Cross-machine assignee dedup (checks GitHub — visible to ALL runners) +# This is the primary guard against duplicate dispatch across machines. +# If another runner already assigned themselves, skip this issue. +if ~/.aidevops/agents/scripts/dispatch-dedup-helper.sh is-assigned NUMBER SLUG "$RUNNER_USER"; then continue; fi + # Assign and dispatch -RUNNER_USER=$(gh api user --jq '.login' 2>/dev/null || whoami) gh issue edit NUMBER --repo SLUG --add-assignee "$RUNNER_USER" --add-label "status:queued" 2>/dev/null || true ~/.aidevops/agents/scripts/headless-runtime-helper.sh run \ diff --git a/.agents/scripts/dispatch-dedup-helper.sh b/.agents/scripts/dispatch-dedup-helper.sh index 210f7bb33f..f65ec0a73e 100755 --- a/.agents/scripts/dispatch-dedup-helper.sh +++ b/.agents/scripts/dispatch-dedup-helper.sh @@ -278,6 +278,72 @@ is_duplicate() { return 1 } +####################################### +# Check if a GitHub issue is already assigned to someone else. +# +# This is the primary cross-machine dedup guard. Process-based checks +# (is_duplicate, has_worker_for_repo_issue) only see local processes — +# they miss workers running on other machines. The GitHub assignee is +# the single source of truth visible to all runners. +# +# Args: +# $1 = issue number +# $2 = repo slug (owner/repo) +# $3 = (optional) current runner login — if assigned to self, not a dup +# Returns: +# exit 0 if assigned to someone else (do NOT dispatch) +# exit 1 if unassigned or assigned to self (safe to dispatch) +# Outputs: assignee info on stdout if assigned +####################################### +is_assigned() { + local issue_number="$1" + local repo_slug="$2" + local self_login="${3:-}" + + if [[ -z "$issue_number" || -z "$repo_slug" ]]; then + # Missing args — cannot check, allow dispatch + return 1 + fi + + # Validate issue number is numeric + if [[ ! "$issue_number" =~ ^[0-9]+$ ]]; then + return 1 + fi + + # Query GitHub for current assignees + local assignees + assignees=$(gh issue view "$issue_number" --repo "$repo_slug" \ + --json assignees --jq '[.assignees[].login] | join(",")' 2>/dev/null) || assignees="" + + if [[ -z "$assignees" ]]; then + # No assignees — safe to dispatch + return 1 + fi + + # If assigned to self, not a duplicate + if [[ -n "$self_login" ]]; then + # Check if ALL assignees are self (could be multiple) + local dominated_by_self=true + local -a assignee_array=() + local saved_ifs="${IFS:-}" + IFS=',' read -ra assignee_array <<<"$assignees" + IFS="$saved_ifs" + local assignee + for assignee in "${assignee_array[@]}"; do + if [[ "$assignee" != "$self_login" ]]; then + dominated_by_self=false + break + fi + done + if [[ "$dominated_by_self" == "true" ]]; then + return 1 + fi + fi + + printf 'ASSIGNED: issue #%s in %s is assigned to %s\n' "$issue_number" "$repo_slug" "$assignees" + return 0 +} + ####################################### # Show help ####################################### @@ -288,6 +354,8 @@ dispatch-dedup-helper.sh - Normalize and deduplicate worker dispatch titles (t23 Usage: dispatch-dedup-helper.sh extract-keys