|
| 1 | +#!/bin/bash |
| 2 | + |
| 3 | +############################################################################## |
| 4 | +# VRMS Stale Issue Verification Skill |
| 5 | +# |
| 6 | +# Purpose: Automatically verify open stale issues (>1 year old) and determine |
| 7 | +# if they should be closed, kept open, or flagged for PM triage |
| 8 | +# |
| 9 | +# Usage: ./verify-stale-issue.sh <issue_number> |
| 10 | +# ./verify-stale-issue.sh 1163 |
| 11 | +# |
| 12 | +# Output: JSON object with verdict, reasoning, and action steps |
| 13 | +############################################################################## |
| 14 | + |
| 15 | +set -e |
| 16 | + |
| 17 | +ISSUE_NUM="${1:-}" |
| 18 | + |
| 19 | +if [ -z "$ISSUE_NUM" ]; then |
| 20 | + echo "Usage: $0 <issue_number>" |
| 21 | + echo "Example: $0 1163" |
| 22 | + exit 1 |
| 23 | +fi |
| 24 | + |
| 25 | +# Helper function to calculate days since date |
| 26 | +days_since() { |
| 27 | + local date_str="$1" |
| 28 | + local date_epoch=$(date -j -f "%Y-%m-%d" "$date_str" "+%s" 2>/dev/null || date -d "$date_str" "+%s" 2>/dev/null || echo 0) |
| 29 | + local now_epoch=$(date "+%s") |
| 30 | + if [ "$date_epoch" -eq 0 ]; then |
| 31 | + echo "unknown" |
| 32 | + else |
| 33 | + echo $(( (now_epoch - date_epoch) / 86400 )) |
| 34 | + fi |
| 35 | +} |
| 36 | + |
| 37 | +# Fetch issue data |
| 38 | +ISSUE_DATA=$(gh issue view "$ISSUE_NUM" --json number,title,state,labels,createdAt,updatedAt,assignees,body,milestone 2>/dev/null) |
| 39 | + |
| 40 | +if [ -z "$ISSUE_DATA" ]; then |
| 41 | + echo "{\"error\": \"Issue #$ISSUE_NUM not found\"}" |
| 42 | + exit 1 |
| 43 | +fi |
| 44 | + |
| 45 | +# Extract fields |
| 46 | +TITLE=$(echo "$ISSUE_DATA" | jq -r '.title') |
| 47 | +STATE=$(echo "$ISSUE_DATA" | jq -r '.state') |
| 48 | +CREATED=$(echo "$ISSUE_DATA" | jq -r '.createdAt' | cut -d'T' -f1) |
| 49 | +UPDATED=$(echo "$ISSUE_DATA" | jq -r '.updatedAt' | cut -d'T' -f1) |
| 50 | +ASSIGNEES=$(echo "$ISSUE_DATA" | jq -r '.assignees[].login' | tr '\n' ',' | sed 's/,$//') |
| 51 | +LABELS=$(echo "$ISSUE_DATA" | jq -r '.labels[].name' | tr '\n' '|') |
| 52 | +BODY=$(echo "$ISSUE_DATA" | jq -r '.body // ""') |
| 53 | + |
| 54 | +# Categorize by labels/metadata |
| 55 | +HAS_DRAFT=$(echo "$LABELS" | grep -q "draft" && echo "true" || echo "false") |
| 56 | +HAS_EPIC=$(echo "$LABELS" | grep -q "Epic\|epic" && echo "true" || echo "false") |
| 57 | +HAS_PM_READY=$(echo "$LABELS" | grep -q "ready for product manager" && echo "true" || echo "false") |
| 58 | +HAS_STAKEHOLDER=$(echo "$TITLE" | grep -q "Stakeholder\|stakeholder" && echo "true" || echo "false") |
| 59 | +HAS_MEETING=$(echo "$LABELS" | grep -q "Agenda\|agenda\|meeting" && echo "true" || echo "false") |
| 60 | +HAS_SECURITY=$(echo "$LABELS" | grep -q "security\|Security" && echo "true" || echo "false") |
| 61 | +HAS_BLOCKED=$(echo "$LABELS" | grep -q "blocked\|Blocked" && echo "true" || echo "false") |
| 62 | + |
| 63 | +DAYS_CREATED=$(days_since "$CREATED") |
| 64 | +DAYS_UPDATED=$(days_since "$UPDATED") |
| 65 | + |
| 66 | +# Decision tree logic |
| 67 | +VERDICT="" |
| 68 | +CATEGORY="" |
| 69 | +REASONING="" |
| 70 | +ACTION="" |
| 71 | +BLOCKER="" |
| 72 | +CONFIDENCE="" |
| 73 | + |
| 74 | +# 1. Check if recurring meeting |
| 75 | +if [ "$HAS_MEETING" = "true" ]; then |
| 76 | + VERDICT="KEEP_OPEN" |
| 77 | + CATEGORY="recurring_process" |
| 78 | + REASONING="This is a recurring meeting agenda or status item" |
| 79 | + ACTION="Keep open - this is a legitimate ongoing process" |
| 80 | + CONFIDENCE="high" |
| 81 | + |
| 82 | +# 2. Check draft status |
| 83 | +elif [ "$HAS_DRAFT" = "true" ] && [ "$HAS_PM_READY" = "false" ]; then |
| 84 | + VERDICT="FLAG_PM" |
| 85 | + CATEGORY="draft_status" |
| 86 | + BLOCKER="DRAFT - not ready for work" |
| 87 | + REASONING="Issue is in draft status and not ready for prioritization" |
| 88 | + ACTION="FLAG FOR PM: Review and decide if draft should be completed or closed" |
| 89 | + CONFIDENCE="high" |
| 90 | + |
| 91 | +# 3. Check if PM decision needed |
| 92 | +elif [ "$HAS_PM_READY" = "true" ]; then |
| 93 | + VERDICT="FLAG_PM" |
| 94 | + CATEGORY="stakeholder_decision" |
| 95 | + BLOCKER="STAKEHOLDER - needs PM approval" |
| 96 | + REASONING="Issue requires PM/product manager review for prioritization or approval" |
| 97 | + ACTION="FLAG FOR PM: Prioritize this issue or decide if it should be closed" |
| 98 | + CONFIDENCE="high" |
| 99 | + |
| 100 | +# 4. Check if stakeholder feedback |
| 101 | +elif [ "$HAS_STAKEHOLDER" = "true" ]; then |
| 102 | + VERDICT="FLAG_PM" |
| 103 | + CATEGORY="stakeholder_decision" |
| 104 | + BLOCKER="STAKEHOLDER - awaiting feedback incorporation" |
| 105 | + REASONING="Issue involves stakeholder input/feedback that needs PM triage" |
| 106 | + ACTION="FLAG FOR PM: Incorporate stakeholder feedback or decide on closure" |
| 107 | + CONFIDENCE="high" |
| 108 | + |
| 109 | +# 5. Check if Epic |
| 110 | +elif [ "$HAS_EPIC" = "true" ]; then |
| 111 | + VERDICT="FLAG_PM" |
| 112 | + CATEGORY="epic_scope" |
| 113 | + BLOCKER="EPIC - needs scope clarification" |
| 114 | + REASONING="Epic issue requires scope review and active management" |
| 115 | + ACTION="FLAG FOR PM: Clarify epic scope, status, and current priority" |
| 116 | + CONFIDENCE="high" |
| 117 | + |
| 118 | +# 6. Check for security issues |
| 119 | +elif [ "$HAS_SECURITY" = "true" ]; then |
| 120 | + VERDICT="VERIFY_CODE" |
| 121 | + CATEGORY="security" |
| 122 | + REASONING="Security issue needs verification of current status/fix" |
| 123 | + ACTION="VERIFY: Check if security concern has been addressed in code" |
| 124 | + CONFIDENCE="medium" |
| 125 | + |
| 126 | +# 7. Check if blocked |
| 127 | +elif [ "$HAS_BLOCKED" = "true" ]; then |
| 128 | + VERDICT="FLAG_PM" |
| 129 | + CATEGORY="blocked" |
| 130 | + BLOCKER="BLOCKED - awaiting dependencies" |
| 131 | + REASONING="Issue is blocked on other work" |
| 132 | + ACTION="FLAG FOR PM: Review blocking issues and unblock if ready" |
| 133 | + CONFIDENCE="high" |
| 134 | + |
| 135 | +# 8. Check if unassigned and very old |
| 136 | +elif [ -z "$ASSIGNEES" ] && [ "$DAYS_CREATED" -gt 900 ] && [ "$DAYS_UPDATED" -gt 180 ]; then |
| 137 | + VERDICT="CLOSE" |
| 138 | + CATEGORY="abandoned" |
| 139 | + REASONING="Unassigned, created ${DAYS_CREATED}+ days ago, no updates in ${DAYS_UPDATED}+ days" |
| 140 | + ACTION="CLOSE: Issue appears abandoned with no recent activity" |
| 141 | + CONFIDENCE="high" |
| 142 | + |
| 143 | +# 9. Default: needs classification |
| 144 | +else |
| 145 | + VERDICT="VERIFY_CODE" |
| 146 | + CATEGORY="needs_classification" |
| 147 | + REASONING="Insufficient data in labels/metadata - requires code verification or PM input" |
| 148 | + ACTION="INVESTIGATE: Check code state and/or flag for PM if still relevant" |
| 149 | + CONFIDENCE="medium" |
| 150 | +fi |
| 151 | + |
| 152 | +# Generate JSON output |
| 153 | +cat <<EOJSON |
| 154 | +{ |
| 155 | + "issue": $ISSUE_NUM, |
| 156 | + "title": $(echo "$TITLE" | jq -R .), |
| 157 | + "state": "$STATE", |
| 158 | + "verdict": "$VERDICT", |
| 159 | + "category": "$CATEGORY", |
| 160 | + "confidence": "$CONFIDENCE", |
| 161 | + "reasoning": $(echo "$REASONING" | jq -R .), |
| 162 | + "action": $(echo "$ACTION" | jq -R .), |
| 163 | + "blocker": $([ -n "$BLOCKER" ] && echo "$BLOCKER" | jq -R . || echo "null"), |
| 164 | + "dates": { |
| 165 | + "created": "$CREATED", |
| 166 | + "updated": "$UPDATED", |
| 167 | + "days_since_created": $DAYS_CREATED, |
| 168 | + "days_since_updated": $DAYS_UPDATED |
| 169 | + }, |
| 170 | + "metadata": { |
| 171 | + "assigned_to": $([ -n "$ASSIGNEES" ] && echo "\"$ASSIGNEES\"" || echo "null"), |
| 172 | + "has_draft": $HAS_DRAFT, |
| 173 | + "has_epic": $HAS_EPIC, |
| 174 | + "has_pm_ready": $HAS_PM_READY, |
| 175 | + "has_stakeholder": $HAS_STAKEHOLDER, |
| 176 | + "has_meeting": $HAS_MEETING, |
| 177 | + "has_security": $HAS_SECURITY, |
| 178 | + "has_blocked": $HAS_BLOCKED |
| 179 | + } |
| 180 | +} |
| 181 | +EOJSON |
0 commit comments