Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions .agents/scripts/issue-sync-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1524,10 +1524,13 @@ cmd_close() {
fi

if gh issue close "$issue_number" --repo "$repo_slug" --comment "$close_comment" 2>/dev/null; then
# Update status label to status:done (t212)
# Update status label to status:done, remove all other status labels (t212, t1009)
gh label create "status:done" --repo "$repo_slug" --color "6F42C1" --description "Task is complete" --force 2>/dev/null || true
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-label "status:done" --remove-label "status:available" --remove-label "status:claimed" --remove-label "status:in-review" 2>/dev/null || true
--add-label "status:done" \
--remove-label "status:available" --remove-label "status:queued" \
--remove-label "status:claimed" --remove-label "status:in-review" \
--remove-label "status:blocked" --remove-label "status:verify-failed" 2>/dev/null || true
print_success "Closed #$issue_number ($task_id)"
closed=$((closed + 1))
else
Expand Down Expand Up @@ -1617,9 +1620,13 @@ _close_single_task() {
fi

if gh issue close "$issue_number" --repo "$repo_slug" --comment "$close_comment" 2>/dev/null; then
# Update status label to status:done, remove all other status labels (t212, t1009)
gh label create "status:done" --repo "$repo_slug" --color "6F42C1" --description "Task is complete" --force 2>/dev/null || true
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-label "status:done" --remove-label "status:available" --remove-label "status:claimed" --remove-label "status:in-review" 2>/dev/null || true
--add-label "status:done" \
--remove-label "status:available" --remove-label "status:queued" \
--remove-label "status:claimed" --remove-label "status:in-review" \
--remove-label "status:blocked" --remove-label "status:verify-failed" 2>/dev/null || true
print_success "Closed #$issue_number ($task_id)"
else
print_error "Failed to close #$issue_number ($task_id)"
Expand Down
228 changes: 222 additions & 6 deletions .agents/scripts/supervisor-helper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2044,6 +2044,10 @@ cmd_add() {
create_github_issue "$task_id" "$description" "$repo"
fi

# t1009: Set status:queued label on the GitHub issue (if it exists)
# This is the initial state — cmd_transition() handles subsequent transitions.
sync_issue_status_label "$task_id" "queued" "" 2>>"${SUPERVISOR_LOG:-/dev/null}" || true

return 0
}

Expand Down Expand Up @@ -2383,6 +2387,10 @@ cmd_transition() {
;;
esac

# t1009: Sync GitHub issue status label on every state transition
# Best-effort — silently skips if gh CLI unavailable or no issue linked
sync_issue_status_label "$task_id" "$new_state" "$current_state" 2>>"${SUPERVISOR_LOG:-/dev/null}" || true

# Auto-generate VERIFY.md entry when task reaches deployed (t180.4)
if [[ "$new_state" == "deployed" ]]; then
generate_verify_entry "$task_id" 2>>"$SUPERVISOR_LOG" || true
Expand Down Expand Up @@ -2960,13 +2968,160 @@ ensure_status_labels() {
fi

# --force updates existing labels without error, creates if missing
# t1009: Full set of status labels for state-transition tracking
gh label create "status:available" --repo "$repo_slug" --color "0E8A16" --description "Task is available for claiming" --force 2>/dev/null || true
gh label create "status:queued" --repo "$repo_slug" --color "C5DEF5" --description "Task is queued for dispatch" --force 2>/dev/null || true
gh label create "status:claimed" --repo "$repo_slug" --color "D93F0B" --description "Task is claimed by a worker" --force 2>/dev/null || true
gh label create "status:in-review" --repo "$repo_slug" --color "FBCA04" --description "Task PR is in review" --force 2>/dev/null || true
gh label create "status:blocked" --repo "$repo_slug" --color "B60205" --description "Task is blocked" --force 2>/dev/null || true
gh label create "status:verify-failed" --repo "$repo_slug" --color "E4E669" --description "Task verification failed" --force 2>/dev/null || true
gh label create "status:done" --repo "$repo_slug" --color "6F42C1" --description "Task is complete" --force 2>/dev/null || true
return 0
}

#######################################
# Map supervisor state to GitHub issue status label (t1009)
# Returns the label name for a given state, empty if no label applies
# (terminal states that close the issue return empty).
# $1: supervisor state
#######################################
state_to_status_label() {
local state="$1"
case "$state" in
queued) echo "status:queued" ;;
dispatched | running | evaluating | retrying) echo "status:claimed" ;;
complete | pr_review | review_triage | merging) echo "status:in-review" ;;
merged | deploying) echo "status:in-review" ;;
blocked) echo "status:blocked" ;;
verify_failed) echo "status:verify-failed" ;;
# Terminal states: verified/deployed close the issue, cancelled closes as not-planned
# These return empty — the caller handles close logic separately
verified | deployed | cancelled | failed) echo "" ;;
*) echo "" ;;
esac
return 0
}

#######################################
# All status labels that can be set on an issue (t1009)
# Used to remove stale labels before applying the new one.
#######################################
ALL_STATUS_LABELS="status:available,status:queued,status:claimed,status:in-review,status:blocked,status:verify-failed,status:done"

#######################################
# Sync GitHub issue status label on state transition (t1009)
# Called from cmd_transition() after each state change.
# Removes all status:* labels, then adds the one matching the new state.
# For terminal states (verified, deployed, cancelled), closes the issue.
# Best-effort: silently skips if gh CLI unavailable or no issue linked.
# $1: task_id
# $2: new_state
# $3: old_state (for logging)
#######################################
sync_issue_status_label() {
local task_id="$1"
local new_state="$2"
local old_state="${3:-}"

# Skip if gh CLI not available or not authenticated
command -v gh &>/dev/null || return 0
check_gh_auth || return 0

# Find the repo path from the task's DB record
local escaped_id
escaped_id=$(sql_escape "$task_id")
local repo_path
repo_path=$(db "$SUPERVISOR_DB" "SELECT repo FROM tasks WHERE id = '$escaped_id';" 2>/dev/null || echo "")
if [[ -z "$repo_path" ]]; then
repo_path=$(find_project_root 2>/dev/null || echo ".")
fi

local issue_number
issue_number=$(find_task_issue_number "$task_id" "$repo_path")
if [[ -z "$issue_number" ]]; then
log_verbose "sync_issue_status_label: no GH issue for $task_id, skipping"
return 0
fi

local repo_slug
repo_slug=$(detect_repo_slug "$repo_path" 2>/dev/null || echo "")
if [[ -z "$repo_slug" ]]; then
return 0
fi

# Ensure all status labels exist on the repo
ensure_status_labels "$repo_slug"

# Determine the new label
local new_label
new_label=$(state_to_status_label "$new_state")

# Build remove args for all status labels except the new one
local -a remove_args=()
local label
while IFS=',' read -ra labels; do
for label in "${labels[@]}"; do
if [[ "$label" != "$new_label" ]]; then
remove_args+=("--remove-label" "$label")
fi
done
done <<<"$ALL_STATUS_LABELS"

# Handle terminal states that close the issue
case "$new_state" in
verified | deployed)
# Close the issue with a completion comment
gh issue close "$issue_number" --repo "$repo_slug" \
--comment "Task $task_id reached state: $new_state (from $old_state)" 2>/dev/null || true
# Add status:done and remove all other status labels
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-label "status:done" "${remove_args[@]}" 2>/dev/null || true
log_verbose "sync_issue_status_label: closed #$issue_number ($task_id -> $new_state)"
return 0
Comment on lines +3055 to +3080
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file structure and locate the relevant functions
wc -l .agents/scripts/supervisor-helper.sh

Repository: marcusquinn/aidevops

Length of output: 106


🏁 Script executed:

# Find the state_to_status_label function definition
rg -n "state_to_status_label" .agents/scripts/supervisor-helper.sh -A 10

Repository: marcusquinn/aidevops

Length of output: 1657


🏁 Script executed:

# Find ALL_STATUS_LABELS definition
rg -n "ALL_STATUS_LABELS" .agents/scripts/supervisor-helper.sh -B 2 -A 2

Repository: marcusquinn/aidevops

Length of output: 709


🏁 Script executed:

# Get the exact lines in question for full context
sed -n '3050,3090p' .agents/scripts/supervisor-helper.sh

Repository: marcusquinn/aidevops

Length of output: 1530


Status label logic conflict in terminal state transitions removes status:done.

When new_state is verified or deployed, state_to_status_label() returns an empty string (line 2997-2998). This causes the removal filter condition [[ "$label" != "$new_label" ]] to be true for all labels in ALL_STATUS_LABELS, including status:done. The subsequent gh issue edit command then attempts to both add and remove status:done in the same operation.

Exclude the terminal target label from the removal list:

Fix: Exclude terminal state target label from removal
  # Determine the new label
  local new_label
  new_label=$(state_to_status_label "$new_state")
+ local target_label="$new_label"
+ case "$new_state" in
+ verified | deployed) target_label="status:done" ;;
+ esac

  # Build remove args for all status labels except the new one
  local -a remove_args=()
  local label
  while IFS=',' read -ra labels; do
    for label in "${labels[@]}"; do
-     if [[ "$label" != "$new_label" ]]; then
+     if [[ "$label" != "$target_label" ]]; then
        remove_args+=("--remove-label" "$label")
      fi
    done
  done <<<"$ALL_STATUS_LABELS"

  verified | deployed)
    # Close the issue with a completion comment
    gh issue close "$issue_number" --repo "$repo_slug" \
      --comment "Task $task_id reached state: $new_state (from $old_state)" 2>/dev/null || true
    # Add status:done and remove all other status labels
    gh issue edit "$issue_number" --repo "$repo_slug" \
-     --add-label "status:done" "${remove_args[@]}" 2>/dev/null || true
+     --add-label "$target_label" "${remove_args[@]}" 2>/dev/null || true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Determine the new label
local new_label
new_label=$(state_to_status_label "$new_state")
# Build remove args for all status labels except the new one
local -a remove_args=()
local label
while IFS=',' read -ra labels; do
for label in "${labels[@]}"; do
if [[ "$label" != "$new_label" ]]; then
remove_args+=("--remove-label" "$label")
fi
done
done <<<"$ALL_STATUS_LABELS"
# Handle terminal states that close the issue
case "$new_state" in
verified | deployed)
# Close the issue with a completion comment
gh issue close "$issue_number" --repo "$repo_slug" \
--comment "Task $task_id reached state: $new_state (from $old_state)" 2>/dev/null || true
# Add status:done and remove all other status labels
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-label "status:done" "${remove_args[@]}" 2>/dev/null || true
log_verbose "sync_issue_status_label: closed #$issue_number ($task_id -> $new_state)"
return 0
# Determine the new label
local new_label
new_label=$(state_to_status_label "$new_state")
local target_label="$new_label"
case "$new_state" in
verified | deployed) target_label="status:done" ;;
esac
# Build remove args for all status labels except the new one
local -a remove_args=()
local label
while IFS=',' read -ra labels; do
for label in "${labels[@]}"; do
if [[ "$label" != "$target_label" ]]; then
remove_args+=("--remove-label" "$label")
fi
done
done <<<"$ALL_STATUS_LABELS"
# Handle terminal states that close the issue
case "$new_state" in
verified | deployed)
# Close the issue with a completion comment
gh issue close "$issue_number" --repo "$repo_slug" \
--comment "Task $task_id reached state: $new_state (from $old_state)" 2>/dev/null || true
# Add status:done and remove all other status labels
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-label "$target_label" "${remove_args[@]}" 2>/dev/null || true
log_verbose "sync_issue_status_label: closed #$issue_number ($task_id -> $new_state)"
return 0
🤖 Prompt for AI Agents
In @.agents/scripts/supervisor-helper.sh around lines 3055 - 3080, The removal
list logic uses new_label from state_to_status_label("$new_state"), but that
returns an empty string for terminal states (verified/deployed) so the loop ends
up scheduling removal of "status:done" and then trying to add it in gh issue
edit; update the loop that builds remove_args to skip empty labels and
explicitly skip "status:done" when new_state is a terminal state
(verified|deployed) so you never add "--remove-label status:done" — reference
the variables and functions new_label, state_to_status_label, ALL_STATUS_LABELS,
remove_args and the gh issue edit call to find where to change the condition.

;;
cancelled)
# Close as not-planned
gh issue close "$issue_number" --repo "$repo_slug" --reason "not planned" \
--comment "Task $task_id cancelled (was: $old_state)" 2>/dev/null || true
# Remove all status labels
gh issue edit "$issue_number" --repo "$repo_slug" \
"${remove_args[@]}" 2>/dev/null || true
log_verbose "sync_issue_status_label: closed #$issue_number as not-planned ($task_id)"
return 0
;;
failed)
# Close with failure comment but don't add status:done
gh issue close "$issue_number" --repo "$repo_slug" \
--comment "Task $task_id failed (was: $old_state)" 2>/dev/null || true
gh issue edit "$issue_number" --repo "$repo_slug" \
"${remove_args[@]}" 2>/dev/null || true
log_verbose "sync_issue_status_label: closed #$issue_number as failed ($task_id)"
return 0
;;
esac

# Non-terminal state: apply the new label, remove all others
if [[ -n "$new_label" ]]; then
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-label "$new_label" "${remove_args[@]}" 2>/dev/null || true
log_verbose "sync_issue_status_label: #$issue_number -> $new_label ($task_id: $old_state -> $new_state)"
fi

# Reopen the issue if it was closed and we're transitioning to a non-terminal state
# (e.g., failed -> queued for retry, blocked -> queued)
if [[ -n "$new_label" ]]; then
local issue_state
issue_state=$(gh issue view "$issue_number" --repo "$repo_slug" --json state --jq '.state' 2>/dev/null || echo "")
if [[ "$issue_state" == "CLOSED" ]]; then
gh issue reopen "$issue_number" --repo "$repo_slug" \
--comment "Task $task_id re-entered pipeline: $old_state -> $new_state" 2>/dev/null || true
log_verbose "sync_issue_status_label: reopened #$issue_number ($task_id: $old_state -> $new_state)"
fi
fi

return 0
}

#######################################
# Find GitHub issue number for a task from TODO.md (t164)
# Outputs the issue number on stdout, empty if not found.
Expand Down Expand Up @@ -3696,16 +3851,22 @@ sync_claim_to_github() {
ensure_status_labels "$repo_slug"

if [[ "$action" == "claim" ]]; then
# t1009: Remove all status labels, add status:claimed
gh issue edit "$issue_number" --repo "$repo_slug" \
--add-assignee "@me" \
--add-label "status:claimed" --remove-label "status:available" 2>/dev/null || true
--add-label "status:claimed" \
--remove-label "status:available" --remove-label "status:queued" \
--remove-label "status:blocked" --remove-label "status:verify-failed" 2>/dev/null || true
elif [[ "$action" == "unclaim" ]]; then
local my_login
my_login=$(gh api user --jq '.login' 2>/dev/null || echo "")
if [[ -n "$my_login" ]]; then
# t1009: Remove all status labels, add status:available
gh issue edit "$issue_number" --repo "$repo_slug" \
--remove-assignee "$my_login" \
--add-label "status:available" --remove-label "status:claimed" 2>/dev/null || true
--add-label "status:available" \
--remove-label "status:claimed" --remove-label "status:queued" \
--remove-label "status:blocked" --remove-label "status:verify-failed" 2>/dev/null || true
Comment on lines +3854 to +3869
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Claim/unclaim paths can leave stale status: labels.*
The removal list is partial, so issues can accumulate multiple status labels (e.g., status:in-review or status:done) and violate the single‑status invariant. Prefer removing all labels from ALL_STATUS_LABELS except the target.

🛠️ Proposed fix (remove all status labels except the target)
 if [[ "$action" == "claim" ]]; then
+   local -a remove_args=()
+   local label
+   while IFS=',' read -ra labels; do
+     for label in "${labels[@]}"; do
+       [[ "$label" != "status:claimed" ]] && remove_args+=("--remove-label" "$label")
+     done
+   done <<<"$ALL_STATUS_LABELS"
     # t1009: Remove all status labels, add status:claimed
     gh issue edit "$issue_number" --repo "$repo_slug" \
       --add-assignee "@me" \
       --add-label "status:claimed" \
-      --remove-label "status:available" --remove-label "status:queued" \
-      --remove-label "status:blocked" --remove-label "status:verify-failed" 2>/dev/null || true
+      "${remove_args[@]}" 2>/dev/null || true
 elif [[ "$action" == "unclaim" ]]; then
   local my_login
   my_login=$(gh api user --jq '.login' 2>/dev/null || echo "")
   if [[ -n "$my_login" ]]; then
+     local -a remove_args=()
+     local label
+     while IFS=',' read -ra labels; do
+       for label in "${labels[@]}"; do
+         [[ "$label" != "status:available" ]] && remove_args+=("--remove-label" "$label")
+       done
+     done <<<"$ALL_STATUS_LABELS"
     # t1009: Remove all status labels, add status:available
     gh issue edit "$issue_number" --repo "$repo_slug" \
       --remove-assignee "$my_login" \
       --add-label "status:available" \
-      --remove-label "status:claimed" --remove-label "status:queued" \
-      --remove-label "status:blocked" --remove-label "status:verify-failed" 2>/dev/null || true
+      "${remove_args[@]}" 2>/dev/null || true
   fi
 fi
🤖 Prompt for AI Agents
In @.agents/scripts/supervisor-helper.sh around lines 3854 - 3869, The
claim/unclaim branches currently remove a hardcoded subset of status labels
which can leave other status:* labels (e.g., status:in-review) behind; update
both the "claim" and "unclaim" paths to remove every label listed in
ALL_STATUS_LABELS except the one you're adding: iterate ALL_STATUS_LABELS (or
build a filtered list) to pass --remove-label for each label not equal to the
target ("status:claimed" for claim, "status:available" for unclaim) while
preserving the existing --add-label and --add-assignee/--remove-assignee
behavior; reference the variables/action names in the snippet (action,
issue_number, repo_slug, my_login, ALL_STATUS_LABELS) so the change replaces the
explicit --remove-label ... sequence with the filtered removal logic.

fi
fi
return 0
Expand Down Expand Up @@ -10841,6 +11002,61 @@ cmd_pulse() {
local remaining=$((issue_sync_interval - elapsed))
log_verbose " Phase 8: Skipped (${remaining}s until next run)"
fi

# Phase 8b: Status label reconciliation sweep (t1009)
# Checks all tasks in the DB and ensures their GitHub issue labels match
# the current supervisor state. Catches drift from missed transitions,
# manual label changes, or failed API calls.
# Piggybacks on the same interval/idle check as Phase 8.
if [[ "$elapsed" -ge "$issue_sync_interval" ]]; then
# Derive repo_slug from sync_repo (set in Phase 8 above)
local rec_repo_slug
rec_repo_slug=$(detect_repo_slug "${sync_repo:-.}" 2>/dev/null || echo "")
if [[ -n "$rec_repo_slug" ]]; then
log_info " Phase 8b: Status label reconciliation sweep"
ensure_status_labels "$rec_repo_slug"
local reconcile_count=0
local reconcile_tasks
reconcile_tasks=$(db "$SUPERVISOR_DB" "SELECT id, status FROM tasks WHERE status NOT IN ('verified','deployed','cancelled','failed');" 2>/dev/null || echo "")
while IFS='|' read -r rec_tid rec_status; do
[[ -z "$rec_tid" ]] && continue
local rec_issue
rec_issue=$(find_task_issue_number "$rec_tid" "${sync_repo:-.}")
[[ -z "$rec_issue" ]] && continue

local expected_label
expected_label=$(state_to_status_label "$rec_status")
[[ -z "$expected_label" ]] && continue

# Check if the issue already has the correct label
local current_labels
current_labels=$(gh issue view "$rec_issue" --repo "$rec_repo_slug" --json labels --jq '[.labels[].name] | join(",")' 2>/dev/null || echo "")
if [[ "$current_labels" != *"$expected_label"* ]]; then
# Build remove args for all status labels except the expected one
local -a rec_remove_args=()
local rec_label
while IFS=',' read -ra rec_labels; do
for rec_label in "${rec_labels[@]}"; do
if [[ "$rec_label" != "$expected_label" ]]; then
rec_remove_args+=("--remove-label" "$rec_label")
fi
done
done <<<"$ALL_STATUS_LABELS"
gh issue edit "$rec_issue" --repo "$rec_repo_slug" \
--add-label "$expected_label" "${rec_remove_args[@]}" 2>/dev/null || true
log_verbose " Phase 8b: Fixed #$rec_issue ($rec_tid): -> $expected_label"
reconcile_count=$((reconcile_count + 1))
fi
done <<<"$reconcile_tasks"
if [[ "$reconcile_count" -gt 0 ]]; then
log_info " Phase 8b: Reconciled $reconcile_count issue label(s)"
else
log_verbose " Phase 8b: All labels in sync"
fi
else
log_verbose " Phase 8b: Skipped (could not detect repo slug)"
fi
fi
fi

# Phase 9: Memory audit pulse (t185)
Expand Down Expand Up @@ -13190,16 +13406,16 @@ cmd_cron() {
local script_path
script_path="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/supervisor-helper.sh"
local cron_marker="# aidevops-supervisor-pulse"

# Detect current PATH for cron environment (t1006)
local user_path="${PATH}"

# Detect GH_TOKEN from gh CLI if available (t1006)
local gh_token=""
if command -v gh &>/dev/null; then
gh_token=$(gh auth token 2>/dev/null || true)
fi

# Build cron command with environment variables
local env_vars=""
if [[ -n "$user_path" ]]; then
Expand All @@ -13208,7 +13424,7 @@ cmd_cron() {
if [[ -n "$gh_token" ]]; then
env_vars="${env_vars:+${env_vars} }GH_TOKEN=${gh_token}"
fi

local cron_cmd="*/${interval} * * * * ${env_vars:+${env_vars} }${script_path} pulse ${batch_arg} >> ${SUPERVISOR_DIR}/cron.log 2>&1 ${cron_marker}"

case "$action" in
Expand Down
27 changes: 27 additions & 0 deletions .agents/scripts/supervisor/issue-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,30 @@ close_completed_issue() {
get_issue_for_task() {
:
}

#######################################
# Map supervisor state to GitHub issue status label (t1009)
# Arguments:
# $1 - supervisor state
# Returns:
# Label name on stdout, empty for terminal states
# Real implementation: supervisor-helper.sh state_to_status_label()
#######################################
state_to_status_label() {
:
}

#######################################
# Sync GitHub issue status label on state transition (t1009)
# Called from cmd_transition() after each state change.
# Removes all status:* labels, adds the one matching the new state.
# For terminal states (verified, deployed, cancelled), closes the issue.
# Arguments:
# $1 - task ID
# $2 - new state
# $3 - old state (for logging)
# Real implementation: supervisor-helper.sh sync_issue_status_label()
#######################################
sync_issue_status_label() {
:
}
Loading