From 5e4113cd9ff709e129fa68ca8616c4211c4640c2 Mon Sep 17 00:00:00 2001 From: AI DevOps Date: Sun, 8 Mar 2026 06:13:30 -0600 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20security=20hardening=20=E2=80=94=20p?= =?UTF-8?q?ath=20traversal,=20arithmetic=20injection,=20awk=20injection,?= =?UTF-8?q?=20JSON=20injection,=20jq=20filter=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security fixes: - #3152: worker-token-helper.sh — validate --token-file path within TOKEN_DIR to prevent path traversal; validate --ttl is numeric to prevent bash arithmetic injection; remove 2>/dev/null from strategy fallback calls - #3179: ai-judgment-helper.sh — use awk -v for safe variable passing (prevents command injection via threshold); use jq for JSON construction (prevents JSON injection via LLM output) - #3240: stuck-detection-helper.sh — fix jq filter to match on both issue number AND repo slug (prevents cross-repo data corruption); use awk -v for safe variable passing; remove 2>/dev/null from gh issue edit calls --- .agents/scripts/ai-judgment-helper.sh | 8 +++-- .agents/scripts/stuck-detection-helper.sh | 14 +++++---- .agents/scripts/worker-token-helper.sh | 36 +++++++++++++++++++++-- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/.agents/scripts/ai-judgment-helper.sh b/.agents/scripts/ai-judgment-helper.sh index 814d57e69..3f41b2715 100755 --- a/.agents/scripts/ai-judgment-helper.sh +++ b/.agents/scripts/ai-judgment-helper.sh @@ -905,9 +905,10 @@ ${user_message}" if [[ -n "$score" ]]; then # Determine pass/fail using awk for float comparison local passed - passed=$(awk "BEGIN { print ($score >= $threshold) ? \"true\" : \"false\" }") + passed=$(awk -v s="$score" -v t="$threshold" 'BEGIN { print (s >= t) ? "true" : "false" }') - local result_json="{\"evaluator\": \"${eval_type}\", \"score\": ${score}, \"passed\": ${passed}, \"details\": \"${details}\"}" + local result_json + result_json=$(jq -n --arg type "$eval_type" --argjson score "${score:-null}" --argjson passed "$passed" --arg details "$details" '{evaluator: $type, score: $score, passed: $passed, details: $details}') # Cache the result cache_judgment "$cache_key" "$result_json" "" "haiku" @@ -919,7 +920,8 @@ ${user_message}" fi # Deterministic fallback: API unavailable - local fallback_json="{\"evaluator\": \"${eval_type}\", \"score\": null, \"passed\": null, \"details\": \"API unavailable, using fallback\"}" + local fallback_json + fallback_json=$(jq -n --arg type "$eval_type" '{evaluator: $type, score: null, passed: null, details: "API unavailable, using fallback"}') echo "$fallback_json" return 0 } diff --git a/.agents/scripts/stuck-detection-helper.sh b/.agents/scripts/stuck-detection-helper.sh index f2e461c33..4f710ae25 100755 --- a/.agents/scripts/stuck-detection-helper.sh +++ b/.agents/scripts/stuck-detection-helper.sh @@ -147,7 +147,7 @@ _sd_write_state() { # Atomic write via temp file + mv local tmp_file="${state_file}.tmp.$$" - if ! printf '%s\n' "$state_json" >"$tmp_file" 2>/dev/null; then + if ! printf '%s\n' "$state_json" >"$tmp_file"; then _sd_log_warn "failed to write temp state file: $tmp_file" rm -f "$tmp_file" 2>/dev/null || true return 1 @@ -268,7 +268,7 @@ cmd_label_stuck() { # Check confidence threshold local above_threshold - above_threshold=$(awk "BEGIN { print ($confidence >= $STUCK_CONFIDENCE_THRESHOLD) ? 1 : 0 }" 2>/dev/null) || above_threshold="0" + above_threshold=$(awk -v c="$confidence" -v t="$STUCK_CONFIDENCE_THRESHOLD" 'BEGIN { print (c >= t) ? 1 : 0 }') || above_threshold="0" if [[ "$above_threshold" -ne 1 ]]; then _sd_log_info "confidence $confidence below threshold $STUCK_CONFIDENCE_THRESHOLD for issue #$issue_number — not labeling" @@ -307,7 +307,7 @@ cmd_label_stuck() { # Apply label gh issue edit "$issue_number" --repo "$repo_slug" \ - --add-label "$STUCK_LABEL" 2>/dev/null || { + --add-label "$STUCK_LABEL" || { _sd_log_warn "failed to add label to issue #$issue_number" return 1 } @@ -331,7 +331,7 @@ ${suggested_actions} *This is an advisory notification only. No automated action has been taken. The worker continues running. The \`${STUCK_LABEL}\` label will be automatically removed if the task completes successfully.*" gh issue comment "$issue_number" --repo "$repo_slug" \ - --body "$comment_body" 2>/dev/null || { + --body "$comment_body" || { _sd_log_warn "failed to comment on issue #$issue_number" } @@ -344,6 +344,7 @@ ${suggested_actions} new_state=$(printf '%s' "$state" | jq \ --arg issue "$issue_number" \ --arg repo "$repo_slug" \ + --arg repo "$repo_slug" \ --arg now "$now" \ '.labeled_issues = ((.labeled_issues // []) + [{"issue": $issue, "repo": $repo, "labeled_at": $now}] | unique_by(.issue + .repo))') || true if [[ -n "$new_state" ]]; then @@ -394,7 +395,7 @@ cmd_label_clear() { # Remove the label gh issue edit "$issue_number" --repo "$repo_slug" \ - --remove-label "$STUCK_LABEL" 2>/dev/null || { + --remove-label "$STUCK_LABEL" || { _sd_log_warn "failed to remove label from issue #$issue_number" return 1 } @@ -416,7 +417,8 @@ cmd_label_clear() { new_state=$(printf '%s' "$state" | jq \ --arg key "$issue_key" \ --arg issue "$issue_number" \ - 'del(.milestones_checked[$key]) | .labeled_issues = [.labeled_issues[] | select(.issue != $issue)]') || true + --arg repo "$repo_slug" \ + 'del(.milestones_checked[$key]) | .labeled_issues = [.labeled_issues[] | select(not (.issue == $issue and .repo == $repo))]') || true if [[ -n "$new_state" ]]; then _sd_write_state "$new_state" || true fi diff --git a/.agents/scripts/worker-token-helper.sh b/.agents/scripts/worker-token-helper.sh index f004f5ac3..6d7c27914 100755 --- a/.agents/scripts/worker-token-helper.sh +++ b/.agents/scripts/worker-token-helper.sh @@ -367,6 +367,16 @@ cmd_create() { ;; --ttl | -t) ttl="$2" + # Validate TTL is numeric to prevent arithmetic injection + if ! [[ "$ttl" =~ ^[0-9]+$ ]]; then + log_token "ERROR" "TTL must be a positive integer: ${ttl}" + return 1 + fi + # Validate TTL is numeric to prevent arithmetic injection + if ! [[ "$ttl" =~ ^[0-9]+$ ]]; then + log_token "ERROR" "TTL must be a positive integer: ${ttl}" + return 1 + fi if ((ttl > MAX_TTL)); then log_token "WARN" "TTL capped at ${MAX_TTL}s (requested ${ttl}s)" ttl=$MAX_TTL @@ -398,14 +408,14 @@ cmd_create() { # Strategy 1: GitHub App installation token (best — enforced by GitHub) local token_file - token_file=$(create_app_token "$repo" "$permissions" "$ttl" 2>/dev/null) && { + token_file=$(create_app_token "$repo" "$permissions" "$ttl") && { log_token "INFO" "Strategy: GitHub App installation token (enforced scoping)" printf '%s' "$token_file" return 0 } # Strategy 2: Delegated token (fallback — advisory scoping) - token_file=$(create_delegated_token "$repo" "$permissions" "$ttl" 2>/dev/null) && { + token_file=$(create_delegated_token "$repo" "$permissions" "$ttl") && { log_token "INFO" "Strategy: Delegated token (advisory scoping)" printf '%s' "$token_file" return 0 @@ -437,6 +447,17 @@ cmd_validate() { return 1 fi + # Validate token file path is within TOKEN_DIR to prevent path traversal + local real_path + real_path=$(realpath "$token_file" 2>/dev/null) || { + log_token "ERROR" "Cannot resolve token file path: ${token_file}" + return 1 + } + if [[ "$real_path" != "${TOKEN_DIR}/"* ]]; then + log_token "ERROR" "Token file must be within ${TOKEN_DIR}: ${token_file}" + return 1 + fi + if [[ ! -f "$token_file" ]]; then log_token "ERROR" "Token file not found: ${token_file}" return 1 @@ -505,6 +526,17 @@ cmd_revoke() { return 1 fi + # Validate token file path is within TOKEN_DIR to prevent path traversal + local real_path + real_path=$(realpath "$token_file" 2>/dev/null) || { + log_token "ERROR" "Cannot resolve token file path: ${token_file}" + return 1 + } + if [[ "$real_path" != "${TOKEN_DIR}/"* ]]; then + log_token "ERROR" "Token file must be within ${TOKEN_DIR}: ${token_file}" + return 1 + fi + local meta_file="${token_file%.token}.meta" local strategy="" local repo="" From 2cd10c724506077ca6bb2d4ab4f82134408734a0 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:05:08 +0000 Subject: [PATCH 2/6] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20dedup,=20compact=20JSON,=20tuple=20unique=5Fby,=20i?= =?UTF-8?q?dempotent=20revoke?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicated TTL validation block in worker-token-helper.sh - Use jq -cn (compact) instead of jq -n to maintain JSONL contract - Fix indentation on fallback_json block in ai-judgment-helper.sh - Remove duplicate --arg repo in stuck-detection-helper.sh - Fix unique_by(.issue + .repo) → unique_by([.issue, .repo]) to prevent string concatenation collisions (e.g. issue 12 + repo 3/x vs issue 1 + repo 23/x) - Fix cmd_revoke realpath to resolve parent dir instead of token file, so cleanup works when only .meta remains (token already deleted) --- .agents/scripts/ai-judgment-helper.sh | 6 +++--- .agents/scripts/stuck-detection-helper.sh | 3 +-- .agents/scripts/worker-token-helper.sh | 18 ++++++++++-------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.agents/scripts/ai-judgment-helper.sh b/.agents/scripts/ai-judgment-helper.sh index 3f41b2715..722d36109 100755 --- a/.agents/scripts/ai-judgment-helper.sh +++ b/.agents/scripts/ai-judgment-helper.sh @@ -908,7 +908,7 @@ ${user_message}" passed=$(awk -v s="$score" -v t="$threshold" 'BEGIN { print (s >= t) ? "true" : "false" }') local result_json - result_json=$(jq -n --arg type "$eval_type" --argjson score "${score:-null}" --argjson passed "$passed" --arg details "$details" '{evaluator: $type, score: $score, passed: $passed, details: $details}') + result_json=$(jq -cn --arg type "$eval_type" --argjson score "${score:-null}" --argjson passed "$passed" --arg details "$details" '{evaluator: $type, score: $score, passed: $passed, details: $details}') # Cache the result cache_judgment "$cache_key" "$result_json" "" "haiku" @@ -920,8 +920,8 @@ ${user_message}" fi # Deterministic fallback: API unavailable - local fallback_json - fallback_json=$(jq -n --arg type "$eval_type" '{evaluator: $type, score: null, passed: null, details: "API unavailable, using fallback"}') + local fallback_json + fallback_json=$(jq -cn --arg type "$eval_type" '{evaluator: $type, score: null, passed: null, details: "API unavailable, using fallback"}') echo "$fallback_json" return 0 } diff --git a/.agents/scripts/stuck-detection-helper.sh b/.agents/scripts/stuck-detection-helper.sh index 4f710ae25..1dd39169e 100755 --- a/.agents/scripts/stuck-detection-helper.sh +++ b/.agents/scripts/stuck-detection-helper.sh @@ -344,9 +344,8 @@ ${suggested_actions} new_state=$(printf '%s' "$state" | jq \ --arg issue "$issue_number" \ --arg repo "$repo_slug" \ - --arg repo "$repo_slug" \ --arg now "$now" \ - '.labeled_issues = ((.labeled_issues // []) + [{"issue": $issue, "repo": $repo, "labeled_at": $now}] | unique_by(.issue + .repo))') || true + '.labeled_issues = ((.labeled_issues // []) + [{"issue": $issue, "repo": $repo, "labeled_at": $now}] | unique_by([.issue, .repo]))') || true if [[ -n "$new_state" ]]; then _sd_write_state "$new_state" || true fi diff --git a/.agents/scripts/worker-token-helper.sh b/.agents/scripts/worker-token-helper.sh index 6d7c27914..d6fc4c966 100755 --- a/.agents/scripts/worker-token-helper.sh +++ b/.agents/scripts/worker-token-helper.sh @@ -372,11 +372,6 @@ cmd_create() { log_token "ERROR" "TTL must be a positive integer: ${ttl}" return 1 fi - # Validate TTL is numeric to prevent arithmetic injection - if ! [[ "$ttl" =~ ^[0-9]+$ ]]; then - log_token "ERROR" "TTL must be a positive integer: ${ttl}" - return 1 - fi if ((ttl > MAX_TTL)); then log_token "WARN" "TTL capped at ${MAX_TTL}s (requested ${ttl}s)" ttl=$MAX_TTL @@ -527,12 +522,19 @@ cmd_revoke() { fi # Validate token file path is within TOKEN_DIR to prevent path traversal - local real_path - real_path=$(realpath "$token_file" 2>/dev/null) || { + # Resolve parent directory instead of the file itself — the token file may + # already be deleted (only .meta remains), and realpath fails on missing files. + local token_dir_real token_parent_real token_base + token_dir_real=$(realpath "$TOKEN_DIR" 2>/dev/null) || { + log_token "ERROR" "Cannot resolve token directory: ${TOKEN_DIR}" + return 1 + } + token_parent_real=$(realpath "$(dirname "$token_file")" 2>/dev/null) || { log_token "ERROR" "Cannot resolve token file path: ${token_file}" return 1 } - if [[ "$real_path" != "${TOKEN_DIR}/"* ]]; then + token_base=$(basename "$token_file") + if [[ "$token_parent_real" != "$token_dir_real" || "$token_base" != *.token ]]; then log_token "ERROR" "Token file must be within ${TOKEN_DIR}: ${token_file}" return 1 fi From 0a39ee6081ad94eb8a978de8502808ee0eb1b522 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:45:50 +0000 Subject: [PATCH 3/6] fix: validate confidence as numeric before awk comparison, canonicalize TOKEN_DIR in cmd_validate - stuck-detection-helper.sh: Add numeric regex validation for confidence and threshold before awk comparison to prevent string comparison semantics (e.g., "high" >= "0.7" is true lexicographically in awk) - worker-token-helper.sh: Canonicalize TOKEN_DIR via realpath in cmd_validate to match the pattern already used in cmd_revoke, preventing false rejections when TOKEN_DIR contains symlinks Addresses remaining CodeRabbit review feedback on PR #3877. --- .agents/scripts/stuck-detection-helper.sh | 7 +++++++ .agents/scripts/worker-token-helper.sh | 9 +++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.agents/scripts/stuck-detection-helper.sh b/.agents/scripts/stuck-detection-helper.sh index 1dd39169e..4bc9548bd 100755 --- a/.agents/scripts/stuck-detection-helper.sh +++ b/.agents/scripts/stuck-detection-helper.sh @@ -267,7 +267,14 @@ cmd_label_stuck() { fi # Check confidence threshold + # Validate confidence and threshold are numeric to prevent string comparison + # semantics in awk (e.g., "high" >= "0.7" is true lexicographically) local above_threshold + if ! [[ "$confidence" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)$ ]] || + ! [[ "$STUCK_CONFIDENCE_THRESHOLD" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)$ ]]; then + _sd_log_error "confidence values must be numeric (got confidence=${confidence}, threshold=${STUCK_CONFIDENCE_THRESHOLD})" + return 1 + fi above_threshold=$(awk -v c="$confidence" -v t="$STUCK_CONFIDENCE_THRESHOLD" 'BEGIN { print (c >= t) ? 1 : 0 }') || above_threshold="0" if [[ "$above_threshold" -ne 1 ]]; then diff --git a/.agents/scripts/worker-token-helper.sh b/.agents/scripts/worker-token-helper.sh index d6fc4c966..d4861285d 100755 --- a/.agents/scripts/worker-token-helper.sh +++ b/.agents/scripts/worker-token-helper.sh @@ -443,12 +443,17 @@ cmd_validate() { fi # Validate token file path is within TOKEN_DIR to prevent path traversal - local real_path + # Canonicalize TOKEN_DIR to handle symlinks (consistent with cmd_revoke) + local real_path token_dir_real + token_dir_real=$(realpath "$TOKEN_DIR" 2>/dev/null) || { + log_token "ERROR" "Cannot resolve token directory: ${TOKEN_DIR}" + return 1 + } real_path=$(realpath "$token_file" 2>/dev/null) || { log_token "ERROR" "Cannot resolve token file path: ${token_file}" return 1 } - if [[ "$real_path" != "${TOKEN_DIR}/"* ]]; then + if [[ "$real_path" != "${token_dir_real}/"* ]]; then log_token "ERROR" "Token file must be within ${TOKEN_DIR}: ${token_file}" return 1 fi From de015d9b6d415524adf5a1a015dc549fc4900074 Mon Sep 17 00:00:00 2001 From: marcusquinn <6428977+marcusquinn@users.noreply.github.com> Date: Sun, 8 Mar 2026 15:54:04 +0000 Subject: [PATCH 4/6] fix: validate numeric params before side effects, guard label_clear against missing state Address remaining CodeRabbit CHANGES_REQUESTED review feedback on PR #3877: - Add elapsed_min to required parameter check in cmd_label_stuck - Validate milestone_min and elapsed_min as positive integers before any GitHub operations or state mutations (prevents jq --argjson failures and comment interpolation of non-numeric values) - Guard cmd_label_clear jq filter against missing .labeled_issues key using null coalescing (// []) to prevent 'Cannot iterate over null' errors that were silently masked by || true --- .agents/scripts/stuck-detection-helper.sh | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.agents/scripts/stuck-detection-helper.sh b/.agents/scripts/stuck-detection-helper.sh index 4bc9548bd..f30420b0e 100755 --- a/.agents/scripts/stuck-detection-helper.sh +++ b/.agents/scripts/stuck-detection-helper.sh @@ -261,14 +261,21 @@ cmd_label_stuck() { local suggested_actions="$6" local repo_slug="${7:-}" - if [[ -z "$issue_number" || -z "$milestone_min" || -z "$confidence" ]]; then + if [[ -z "$issue_number" || -z "$milestone_min" || -z "$elapsed_min" || -z "$confidence" ]]; then _sd_log_error "usage: label-stuck [--repo ]" return 1 fi + # Validate numeric parameters before any side effects (GitHub ops, state mutations). + # milestone_min/elapsed_min are integers used in jq --argjson and comment interpolation. + # confidence is a float used in awk comparison — non-numeric strings cause + # lexicographic semantics (e.g., "high" >= "0.7" is true). + if ! [[ "$milestone_min" =~ ^[0-9]+$ ]] || ! [[ "$elapsed_min" =~ ^[0-9]+$ ]]; then + _sd_log_error "milestone_min and elapsed_min must be positive integers (got milestone=${milestone_min}, elapsed=${elapsed_min})" + return 1 + fi + # Check confidence threshold - # Validate confidence and threshold are numeric to prevent string comparison - # semantics in awk (e.g., "high" >= "0.7" is true lexicographically) local above_threshold if ! [[ "$confidence" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)$ ]] || ! [[ "$STUCK_CONFIDENCE_THRESHOLD" =~ ^([0-9]+([.][0-9]+)?|[.][0-9]+)$ ]]; then @@ -424,7 +431,7 @@ cmd_label_clear() { --arg key "$issue_key" \ --arg issue "$issue_number" \ --arg repo "$repo_slug" \ - 'del(.milestones_checked[$key]) | .labeled_issues = [.labeled_issues[] | select(not (.issue == $issue and .repo == $repo))]') || true + 'del(.milestones_checked[$key]) | .labeled_issues = [(.labeled_issues // [])[] | select((.issue == $issue and .repo == $repo) | not)]') || true if [[ -n "$new_state" ]]; then _sd_write_state "$new_state" || true fi From e03d1968fd6d9144fd2d023b480e09434e192d9b Mon Sep 17 00:00:00 2001 From: AI DevOps Date: Sun, 8 Mar 2026 11:43:33 -0600 Subject: [PATCH 5/6] fix: address CodeRabbit review feedback on batch 3 - ai-judgment-helper: use jq -cn for compact one-record-per-line output - stuck-detection-helper: validate elapsed_min/milestone_min/confidence as numeric - stuck-detection-helper: fix unique_by string concat collision, use tuple key - stuck-detection-helper: handle missing .labeled_issues key in clear filter - worker-token-helper: resolve TOKEN_DIR with realpath for symlink consistency - worker-token-helper: handle missing token file gracefully in cmd_revoke --- .agents/scripts/stuck-detection-helper.sh | 6 +++++- .agents/scripts/worker-token-helper.sh | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.agents/scripts/stuck-detection-helper.sh b/.agents/scripts/stuck-detection-helper.sh index f30420b0e..b1a7a771a 100755 --- a/.agents/scripts/stuck-detection-helper.sh +++ b/.agents/scripts/stuck-detection-helper.sh @@ -274,6 +274,10 @@ cmd_label_stuck() { _sd_log_error "milestone_min and elapsed_min must be positive integers (got milestone=${milestone_min}, elapsed=${elapsed_min})" return 1 fi + if ! [[ "$confidence" =~ ^[0-9]*\.?[0-9]+$ ]]; then + _sd_log_error "confidence must be a number, got: ${confidence}" + return 1 + fi # Check confidence threshold local above_threshold @@ -431,7 +435,7 @@ cmd_label_clear() { --arg key "$issue_key" \ --arg issue "$issue_number" \ --arg repo "$repo_slug" \ - 'del(.milestones_checked[$key]) | .labeled_issues = [(.labeled_issues // [])[] | select((.issue == $issue and .repo == $repo) | not)]') || true + 'del(.milestones_checked[$key]) | .labeled_issues = [(.labeled_issues // [])[] | select((.issue != $issue) or (.repo != $repo))]') || true if [[ -n "$new_state" ]]; then _sd_write_state "$new_state" || true fi diff --git a/.agents/scripts/worker-token-helper.sh b/.agents/scripts/worker-token-helper.sh index d4861285d..25412f13a 100755 --- a/.agents/scripts/worker-token-helper.sh +++ b/.agents/scripts/worker-token-helper.sh @@ -443,7 +443,7 @@ cmd_validate() { fi # Validate token file path is within TOKEN_DIR to prevent path traversal - # Canonicalize TOKEN_DIR to handle symlinks (consistent with cmd_revoke) + # Canonicalize both paths with realpath to handle symlinked home directories local real_path token_dir_real token_dir_real=$(realpath "$TOKEN_DIR" 2>/dev/null) || { log_token "ERROR" "Cannot resolve token directory: ${TOKEN_DIR}" From 9a61e306aaa2e70d765eaa324a05d403dc846b06 Mon Sep 17 00:00:00 2001 From: AI DevOps Date: Sun, 8 Mar 2026 12:37:35 -0600 Subject: [PATCH 6/6] fix: propagate advisory-comment failure from label-stuck Capture gh issue comment failure status and return it after state recording completes. State is always recorded regardless of comment success (since the label was already applied), preventing re-labeling on the next check cycle. --- .agents/scripts/stuck-detection-helper.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.agents/scripts/stuck-detection-helper.sh b/.agents/scripts/stuck-detection-helper.sh index b1a7a771a..faa34a38c 100755 --- a/.agents/scripts/stuck-detection-helper.sh +++ b/.agents/scripts/stuck-detection-helper.sh @@ -348,12 +348,15 @@ ${suggested_actions} --- *This is an advisory notification only. No automated action has been taken. The worker continues running. The \`${STUCK_LABEL}\` label will be automatically removed if the task completes successfully.*" + local comment_failed=0 gh issue comment "$issue_number" --repo "$repo_slug" \ --body "$comment_body" || { _sd_log_warn "failed to comment on issue #$issue_number" + comment_failed=1 } - # Record milestone and labeled issue in state + # Record milestone and labeled issue in state (regardless of comment success, + # since the label was applied — skipping state would cause re-labeling). _sd_record_milestone "$issue_number" "$milestone_min" "$repo_slug" || true local state @@ -369,7 +372,7 @@ ${suggested_actions} fi _sd_log_warn "labeled issue #$issue_number as stuck (confidence: $confidence, milestone: ${milestone_min}min)" - return 0 + return "$comment_failed" } # Remove stuck-detection label from a GitHub issue on task success.