-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcode.sh
More file actions
executable file
·3179 lines (2632 loc) · 122 KB
/
Copy pathcode.sh
File metadata and controls
executable file
·3179 lines (2632 loc) · 122 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
#
# code.sh - Automated Feature Development Loop
#
# This script orchestrates multiple Claude Code sessions to automate the
# Feature Development Workflow from CLAUDE.md. It fetches GitHub issues,
# plans implementation, codes, tests, reviews, and deploys.
#
# Features:
# - GitHub-based memory system (labels for phase, comments for session data)
# - Crash recovery and resume from any phase
# - Session cost tracking and audit trail
# - Multi-machine support (all state in GitHub)
#
# Session Architecture:
# 0. User Feedback Triage (clean context - runs first each cycle)
# 1. Issue Selection (clean context)
# 2. Planning (clean context)
# 3. Implementation + Testing + PR (shared context - tight feedback loop)
# 4. Code Review (CLEAN CONTEXT - critical for quality!)
# 5. Fix Review Feedback (if needed)
# 6. Merge + Deploy + Verify (shared context)
# 7. Documentation (optional)
#
# Usage:
# ./code.sh # Run continuous loop (triage → select → dev)
# ./code.sh --once # Run single cycle including triage
# ./code.sh -i 42 # Work on specific issue (implies --once)
# ./code.sh --issue 42 # Same as above
# ./code.sh --resume # Resume in-progress work only
# ./code.sh --status # Show triage and dev status
# ./code.sh --triage # Triage-only mode, then exit
# ./code.sh --hint "..." # Provide priority hint for issue selection
# ./code.sh --init # Run initial setup (GitHub labels, etc.) - once per repo
#
# Examples with --hint:
# ./code.sh --hint "Work on issue #42 first, then #46, then #58"
# ./code.sh --hint "Focus on countdown timer bugs before other issues"
# ./code.sh --hint "Prioritize issues #42, #46, #58, #97 in that order"
#
# Note: Script operates on current working directory, so you can run it
# from any project: cd /path/to/project && /path/to/code.sh
#
set -euo pipefail
# Configuration
# Use current working directory as project root (allows running from any project)
REPO_ROOT="$(pwd)"
STATE_DIR="$REPO_ROOT/.auto-dev"
LOG_FILE="$STATE_DIR/auto-dev.log"
SINGLE_CYCLE=false
RESUME_ONLY=false
SHOW_STATUS=false
RUN_INIT=false
TARGET_ISSUE=""
SELECTION_HINT=""
RUN_TRIAGE_ONLY=false
MAX_REVIEW_ROUNDS=10
MAX_CI_FIX_ATTEMPTS=5
HUMAN_REVIEW_TIMEOUT=60 # Minutes to wait for human reviewers
HUMAN_REVIEW_POLL=60 # Seconds between polling for human reviews
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--once)
SINGLE_CYCLE=true
shift
;;
--issue|-i)
if [ -z "${2:-}" ] || [[ "$2" == -* ]]; then
echo "Error: --issue requires an issue number"
exit 1
fi
TARGET_ISSUE="$2"
SINGLE_CYCLE=true # Implies --once
shift 2
;;
--resume)
RESUME_ONLY=true
shift
;;
--status)
SHOW_STATUS=true
shift
;;
--init)
RUN_INIT=true
shift
;;
--hint|-h)
if [ -z "${2:-}" ]; then
echo "Error: --hint requires a string argument"
exit 1
fi
SELECTION_HINT="$2"
shift 2
;;
--triage)
RUN_TRIAGE_ONLY=true
shift
;;
*)
echo "Unknown option: $1"
echo "Usage: $0 [--once] [-i|--issue <number>] [--resume] [--status] [--init] [--hint \"...\"] [--triage]"
exit 1
;;
esac
done
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# Logging functions (output to stderr to avoid polluting stdout captures)
log() { echo -e "${BLUE}[$(date +'%H:%M:%S')]${NC} $*" | tee -a "$LOG_FILE" >&2; }
success() { echo -e "${GREEN}[$(date +'%H:%M:%S')] ✓${NC} $*" | tee -a "$LOG_FILE" >&2; }
warn() { echo -e "${YELLOW}[$(date +'%H:%M:%S')] ⚠${NC} $*" | tee -a "$LOG_FILE" >&2; }
error() { echo -e "${RED}[$(date +'%H:%M:%S')] ✗${NC} $*" | tee -a "$LOG_FILE" >&2; }
header() { echo -e "\n${BOLD}${CYAN}$*${NC}" | tee -a "$LOG_FILE" >&2; }
# Detect rate limit errors by parsing JSON output from Claude Code CLI
# Returns 0 if rate limit error detected, 1 otherwise
# Checks for:
# - API errors: {"type":"error","error":{"type":"rate_limit_error"|"overloaded_error",...}}
# - SDK result errors: {"type":"result","subtype":"error_during_execution","errors":[...]}
# where errors array contains rate limit related messages
# - Hook denials: {"decision":"deny","reason":"Rate limit exceeded"} (exit code 2)
# Global flag to track if prompt-too-long error occurred
# This is set by detect_prompt_too_long_error and checked by callers
PROMPT_TOO_LONG_ERROR=false
# Detect "Prompt is too long" errors by parsing JSON output from Claude Code CLI
# Returns 0 if prompt-too-long error detected, 1 otherwise
# Sets PROMPT_TOO_LONG_ERROR=true when detected
detect_prompt_too_long_error() {
local output_file="$1"
# Check for the specific error message in the output
if grep -qiE "Prompt is too long|prompt.*too.*long|context.*too.*long|token.*limit.*exceeded" "$output_file" 2>/dev/null; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] PROMPT TOO LONG ERROR DETECTED" >> "$LOG_FILE"
PROMPT_TOO_LONG_ERROR=true
return 0
fi
# Also check JSON errors for token/context limit issues
while IFS= read -r line; do
[[ "$line" != "{"* ]] && continue
local msg_type error_msg
msg_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null) || continue
if [ "$msg_type" = "error" ] || [ "$msg_type" = "result" ]; then
error_msg=$(echo "$line" | jq -r '.error.message // .errors[]? // empty' 2>/dev/null) || true
if echo "$error_msg" | grep -qiE "prompt.*too.*long|token.*limit|context.*length"; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] PROMPT TOO LONG ERROR DETECTED: $error_msg" >> "$LOG_FILE"
PROMPT_TOO_LONG_ERROR=true
return 0
fi
fi
done < "$output_file"
return 1
}
detect_rate_limit_error() {
local output_file="$1"
# Parse each JSON line looking for rate limit indicators
while IFS= read -r line; do
# Skip non-JSON lines
[[ "$line" != "{"* ]] && continue
# Try to parse as JSON, skip if invalid
local json_valid
json_valid=$(echo "$line" | jq -e . 2>/dev/null) || continue
# Check for hook denial (decision: deny with rate limit reason)
local decision reason
decision=$(echo "$line" | jq -r '.decision // empty' 2>/dev/null) || true
if [ "$decision" = "deny" ]; then
reason=$(echo "$line" | jq -r '.reason // empty' 2>/dev/null) || true
if echo "$reason" | grep -qiE "(rate.?limit|too many|exceeded|throttl)"; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] RATE LIMIT DETECTED: Hook denial reason='$reason'" >> "$LOG_FILE"
return 0
fi
fi
# Check for API-level error (type: "error")
local msg_type error_type
msg_type=$(echo "$line" | jq -r '.type // empty' 2>/dev/null) || true
if [ "$msg_type" = "error" ]; then
error_type=$(echo "$line" | jq -r '.error.type // empty' 2>/dev/null) || true
case "$error_type" in
rate_limit_error|overloaded_error)
echo "[$(date +'%Y-%m-%d %H:%M:%S')] RATE LIMIT DETECTED: API error type=$error_type" >> "$LOG_FILE"
return 0
;;
esac
fi
# Check for SDK result with error subtype
if [ "$msg_type" = "result" ]; then
local subtype
subtype=$(echo "$line" | jq -r '.subtype // empty' 2>/dev/null) || true
if [ "$subtype" = "error_during_execution" ]; then
# Check if errors array contains rate limit messages
local errors
errors=$(echo "$line" | jq -r '.errors // [] | .[]' 2>/dev/null) || true
if echo "$errors" | grep -qiE "(rate.?limit|429|overloaded|too many requests)"; then
echo "[$(date +'%Y-%m-%d %H:%M:%S')] RATE LIMIT DETECTED: SDK error_during_execution with rate limit message" >> "$LOG_FILE"
return 0
fi
fi
fi
done < "$output_file"
return 1
}
# Initialize state directory
mkdir -p "$STATE_DIR"
#═══════════════════════════════════════════════════════════════════════════════
# CLEANUP AND EXIT HANDLING
# Ensures background processes (dev servers, etc.) are stopped on exit
#═══════════════════════════════════════════════════════════════════════════════
# Track background PIDs started by this script
BACKGROUND_PIDS=()
# Cleanup function - kills any background processes we started
cleanup_background_processes() {
log "Cleaning up background processes..."
# Kill any tracked background PIDs (handle empty array with ${arr[@]+"${arr[@]}"} pattern)
if [ ${#BACKGROUND_PIDS[@]} -gt 0 ]; then
for pid in "${BACKGROUND_PIDS[@]}"; do
if kill -0 "$pid" 2>/dev/null; then
log "Killing background process $pid"
kill "$pid" 2>/dev/null || true
fi
done
fi
# Kill any dev servers on port 3000 (common testing port)
local port_pids
port_pids=$(lsof -ti:3000 2>/dev/null || true)
if [ -n "$port_pids" ]; then
log "Killing processes on port 3000: $port_pids"
echo "$port_pids" | xargs kill 2>/dev/null || true
fi
# Kill any node processes started by npm run dev in this directory
local npm_pids
npm_pids=$(pgrep -f "node.*$REPO_ROOT" 2>/dev/null || true)
if [ -n "$npm_pids" ]; then
log "Killing node processes for this project: $npm_pids"
echo "$npm_pids" | xargs kill 2>/dev/null || true
fi
success "Cleanup complete"
}
# Set up trap to run cleanup on exit
trap cleanup_background_processes EXIT
#═══════════════════════════════════════════════════════════════════════════════
# GITHUB MEMORY SYSTEM
# Uses labels for phase tracking, comments for session memory
#═══════════════════════════════════════════════════════════════════════════════
# Label definitions: name|color|description (using | as delimiter since : is in label names)
PHASE_LABELS=(
"auto-dev:selecting|0E8A16|Being selected for development"
"auto-dev:planning|1D76DB|Creating implementation plan"
"auto-dev:implementing|5319E7|Writing code and testing"
"auto-dev:pr-waiting|FBCA04|PR created, waiting for CI"
"auto-dev:reviewing|D93F0B|Under code review"
"auto-dev:fixing|F9D0C4|Addressing review feedback"
"auto-dev:merging|0052CC|Being merged and deployed"
"auto-dev:verifying|BFD4F2|Production verification"
"auto-dev:complete|0E8A16|Successfully completed"
"auto-dev:blocked|B60205|Needs manual intervention"
"auto-dev:ci-failed|FBCA04|CI checks failing, attempting fixes"
)
# Signal labels: used by Claude sessions to signal completion status
# These are consumed (removed) after being read by the script
SIGNAL_LABELS=(
"auto-dev:signal:review-approved|0E8A16|Review approved, ready to merge"
"auto-dev:signal:review-changes|D93F0B|Review requests changes"
"auto-dev:signal:needs-update|FBCA04|PR needs updates after verification"
)
# Triage labels: for user-feedback issue triage workflow
TRIAGE_LABELS=(
"auto-dev:triage:pending|C5DEF5|Feedback awaiting triage"
"auto-dev:triage:analyzing|1D76DB|Being analyzed for scope"
"auto-dev:triage:complete|0E8A16|Triage complete, issues created"
"auto-dev:triage:blocked|B60205|Triage needs manual intervention"
)
# Ensure all required labels exist in the repo with correct colors and descriptions
ensure_labels_exist() {
log "Ensuring GitHub labels exist..."
local created=0
local updated=0
# Create phase labels
for label_spec in "${PHASE_LABELS[@]}"; do
IFS='|' read -r name color desc <<< "$label_spec"
if gh label create "$name" --color "$color" --description "$desc" 2>/dev/null; then
created=$((created + 1))
else
gh label edit "$name" --color "$color" --description "$desc" 2>/dev/null && updated=$((updated + 1))
fi
done
# Create signal labels
for label_spec in "${SIGNAL_LABELS[@]}"; do
IFS='|' read -r name color desc <<< "$label_spec"
if gh label create "$name" --color "$color" --description "$desc" 2>/dev/null; then
created=$((created + 1))
else
gh label edit "$name" --color "$color" --description "$desc" 2>/dev/null && updated=$((updated + 1))
fi
done
# Create triage labels
for label_spec in "${TRIAGE_LABELS[@]}"; do
IFS='|' read -r name color desc <<< "$label_spec"
if gh label create "$name" --color "$color" --description "$desc" 2>/dev/null; then
created=$((created + 1))
else
gh label edit "$name" --color "$color" --description "$desc" 2>/dev/null && updated=$((updated + 1))
fi
done
if [ $created -gt 0 ] || [ $updated -gt 0 ]; then
log "Labels: $created created, $updated updated"
fi
}
# Check if a signal label is set on an issue
has_signal() {
local issue_num=$1
local signal=$2
local labels
labels=$(gh issue view "$issue_num" --json labels -q '.labels[].name' 2>/dev/null || echo "")
echo "$labels" | grep -q "^auto-dev:signal:$signal$"
}
# Set a signal label on an issue
set_signal() {
local issue_num=$1
local signal=$2
gh label create "auto-dev:signal:$signal" --color "CCCCCC" 2>/dev/null || true
gh issue edit "$issue_num" --add-label "auto-dev:signal:$signal" >/dev/null 2>&1 || true
}
# Clear a signal label from an issue (consume the signal)
clear_signal() {
local issue_num=$1
local signal=$2
gh issue edit "$issue_num" --remove-label "auto-dev:signal:$signal" >/dev/null 2>&1 || true
}
# Clear all signal labels from an issue
clear_all_signals() {
local issue_num=$1
local labels
labels=$(gh issue view "$issue_num" --json labels -q '.labels[].name' 2>/dev/null | grep "^auto-dev:signal:" || true)
for label in $labels; do
gh issue edit "$issue_num" --remove-label "$label" >/dev/null 2>&1 || true
done
}
# Set workflow phase for an issue (removes old phase, adds new)
set_phase() {
local issue_num=$1
local phase=$2
# Remove all existing auto-dev phase labels
local existing_labels
existing_labels=$(gh issue view "$issue_num" --json labels -q '.labels[].name' 2>/dev/null | grep "^auto-dev:" || true)
for old_label in $existing_labels; do
# Keep metadata labels (pr:, branch:, round:, cost:), remove phase labels
if [[ "$old_label" =~ ^auto-dev:(selecting|planning|implementing|pr-waiting|reviewing|fixing|merging|verifying|complete|blocked|ci-failed)$ ]]; then
gh issue edit "$issue_num" --remove-label "$old_label" >/dev/null 2>&1 || true
fi
done
# Add new phase label
gh issue edit "$issue_num" --add-label "auto-dev:$phase" >/dev/null 2>&1 || true
log "Phase → ${MAGENTA}$phase${NC} for issue #$issue_num"
}
# Get current phase of an issue
get_phase() {
local issue_num=$1
local labels
labels=$(gh issue view "$issue_num" --json labels -q '.labels[].name' 2>/dev/null || echo "")
for phase in selecting planning implementing pr-waiting reviewing fixing merging verifying complete blocked ci-failed; do
if echo "$labels" | grep -q "^auto-dev:$phase$"; then
echo "$phase"
return 0
fi
done
echo ""
}
# Add/update a metadata label (branch, round, cost)
set_metadata() {
local issue_num=$1
local key=$2
local value=$3
# Remove existing label with same key prefix
local existing
existing=$(gh issue view "$issue_num" --json labels -q ".labels[].name" 2>/dev/null | grep "^auto-dev:$key:" || true)
if [ -n "$existing" ]; then
gh issue edit "$issue_num" --remove-label "$existing" >/dev/null 2>&1 || true
fi
# Create and add new label
local label_name="auto-dev:$key:$value"
gh label create "$label_name" --color "CCCCCC" 2>/dev/null || true
gh issue edit "$issue_num" --add-label "$label_name" >/dev/null 2>&1 || true
}
# Get metadata value from labels
# Returns empty string if metadata doesn't exist (safe with set -eo pipefail)
get_metadata() {
local issue_num=$1
local key=$2
local labels
labels=$(gh issue view "$issue_num" --json labels -q ".labels[].name" 2>/dev/null) || true
echo "$labels" | grep "^auto-dev:$key:" | sed "s/auto-dev:$key://" | head -1 || true
}
# Validate and sanitize a PR number
# Returns clean numeric PR number or empty string if invalid
# Usage: clean_pr=$(validate_pr_number "$pr_num")
validate_pr_number() {
local input=$1
# Extract only the numeric part (last number in the string)
local num
num=$(echo "$input" | grep -oE '[0-9]+' | tail -1)
if [ -n "$num" ] && [ "$num" -gt 0 ] 2>/dev/null; then
echo "$num"
else
echo ""
fi
}
# Post session memory as a structured comment
post_session_memory() {
local issue_num=$1
local phase_name=$2
local session_start=$3
local session_end=$4
local cost=$5
local summary=$6
local extra_info=${7:-""}
local duration=$((session_end - session_start))
local duration_min=$((duration / 60))
local duration_sec=$((duration % 60))
local duration_fmt="${duration_min}m ${duration_sec}s"
# Format timestamps
local start_fmt end_fmt
if date --version 2>/dev/null | grep -q GNU; then
start_fmt=$(date -d "@$session_start" -u +"%Y-%m-%dT%H:%M:%SZ")
end_fmt=$(date -d "@$session_end" -u +"%Y-%m-%dT%H:%M:%SZ")
else
start_fmt=$(date -r "$session_start" -u +"%Y-%m-%dT%H:%M:%SZ")
end_fmt=$(date -r "$session_end" -u +"%Y-%m-%dT%H:%M:%SZ")
fi
local session_id="session-$(date +%s)-$$"
local comment="## 🤖 Auto-Dev Session: $phase_name
| Field | Value |
|-------|-------|
| **Session ID** | \`$session_id\` |
| **Started** | $start_fmt |
| **Completed** | $end_fmt |
| **Duration** | $duration_fmt |
| **Cost** | \$$cost |
### Summary
$summary"
if [ -n "$extra_info" ]; then
comment+="
### Details
$extra_info"
fi
comment+="
---
<sub>🤖 Automated by auto-dev</sub>"
gh issue comment "$issue_num" --body "$comment" >/dev/null 2>&1 || warn "Failed to post session memory"
}
# Get accumulated cost from all session comments
get_accumulated_cost() {
local issue_num=$1
# Extract all costs from session comments and sum them
local total
total=$(gh issue view "$issue_num" --comments --json comments \
-q '[.comments[].body | capture("\\*\\*Cost\\*\\* \\| \\$(?<cost>[0-9.]+)") | .cost | tonumber] | add // 0' 2>/dev/null)
printf "%.2f" "${total:-0}"
}
# Find issues that are in-progress (have auto-dev phase labels)
find_in_progress_issues() {
local phases=("selecting" "planning" "implementing" "pr-waiting" "reviewing" "fixing" "merging" "verifying")
for phase in "${phases[@]}"; do
local issues
issues=$(gh issue list --label "auto-dev:$phase" --json number,title -q '.[] | "\(.number):\(.title)"' 2>/dev/null || echo "")
if [ -n "$issues" ]; then
while IFS= read -r line; do
local num title
num=$(echo "$line" | cut -d: -f1)
title=$(echo "$line" | cut -d: -f2-)
echo "$num|$phase|$title"
done <<< "$issues"
fi
done
}
# Find the most actionable in-progress issue
find_resumable_issue() {
# Priority order for resuming
local phases=("fixing" "reviewing" "pr-waiting" "implementing" "planning" "merging" "verifying" "selecting")
for phase in "${phases[@]}"; do
local issue
issue=$(gh issue list --label "auto-dev:$phase" --json number -q '.[0].number' 2>/dev/null || echo "")
if [ -n "$issue" ]; then
echo "$issue"
return 0
fi
done
echo ""
}
# Show status of all in-progress issues
show_status() {
header "Auto-Dev Status"
# Show triage status first
local untriaged triage_in_progress
untriaged=$(find_untriaged_feedback_issues)
triage_in_progress=$(find_triage_in_progress)
if [ -n "$untriaged" ] || [ -n "$triage_in_progress" ]; then
echo ""
echo -e "${BOLD}Triage Queue:${NC}"
printf "%-6s %-20s %-45s\n" "ISSUE" "STATUS" "TITLE"
printf "%-6s %-20s %-45s\n" "-----" "------" "-----"
if [ -n "$untriaged" ]; then
while IFS='|' read -r num title; do
[ -z "$num" ] && continue
printf "%-6s %-20s %-45s\n" "#$num" "awaiting triage" "${title:0:45}"
done <<< "$untriaged"
fi
if [ -n "$triage_in_progress" ]; then
while IFS='|' read -r num phase title; do
[ -z "$num" ] && continue
printf "%-6s %-20s %-45s\n" "#$num" "$phase" "${title:0:45}"
done <<< "$triage_in_progress"
fi
echo ""
fi
# Show development status
local in_progress
in_progress=$(find_in_progress_issues)
if [ -z "$in_progress" ] && [ -z "$untriaged" ] && [ -z "$triage_in_progress" ]; then
log "No in-progress issues found"
return 0
fi
if [ -n "$in_progress" ]; then
echo -e "${BOLD}Development Queue:${NC}"
printf "%-6s %-15s %-50s\n" "ISSUE" "PHASE" "TITLE"
printf "%-6s %-15s %-50s\n" "-----" "-----" "-----"
while IFS='|' read -r num phase title; do
local pr_num branch cost
pr_num=$(get_linked_pr "$num" 2>/dev/null) || true
cost=$(get_accumulated_cost "$num")
printf "%-6s %-15s %-50s\n" "#$num" "$phase" "${title:0:50}"
if [ -n "$pr_num" ]; then
printf " └─ PR #%s, Cost: \$%s\n" "$pr_num" "$cost"
fi
done <<< "$in_progress"
echo ""
fi
}
# Mark issue as blocked
mark_blocked() {
local issue_num=$1
local reason=$2
set_phase "$issue_num" "blocked"
gh issue comment "$issue_num" --body "## ⚠️ Auto-Dev Blocked
**Reason:** $reason
**Time:** $(date -u +"%Y-%m-%dT%H:%M:%SZ")
### To Resume
1. Fix the underlying issue
2. Remove the \`auto-dev:blocked\` label
3. Add the appropriate phase label to continue from:
- \`auto-dev:implementing\` - to restart implementation
- \`auto-dev:reviewing\` - to restart code review
- etc.
---
<sub>🤖 Automated by auto-dev</sub>" >/dev/null 2>&1 || true
error "Issue #$issue_num blocked: $reason"
}
#═══════════════════════════════════════════════════════════════════════════════
# USER FEEDBACK TRIAGE SYSTEM
# Processes issues labeled 'user-feedback' into atomic development tasks
#═══════════════════════════════════════════════════════════════════════════════
# Find user-feedback issues that haven't been triaged yet
# Returns: issue_num|title format, one per line
find_untriaged_feedback_issues() {
# Find issues with 'user-feedback' label but NO triage labels
local feedback_issues
feedback_issues=$(gh issue list --label "user-feedback" --state open --json number,title,labels \
-q '.[] | select(.labels | map(.name) | any(startswith("auto-dev:triage:")) | not) | "\(.number)|\(.title)"' 2>/dev/null || echo "")
echo "$feedback_issues"
}
# Find user-feedback issues currently being triaged
find_triage_in_progress() {
local phases=("pending" "analyzing")
for phase in "${phases[@]}"; do
local issues
issues=$(gh issue list --label "auto-dev:triage:$phase" --json number,title -q '.[] | "\(.number)|\(.title)"' 2>/dev/null || echo "")
if [ -n "$issues" ]; then
while IFS= read -r line; do
local num title
num=$(echo "$line" | cut -d'|' -f1)
title=$(echo "$line" | cut -d'|' -f2-)
echo "$num|triage:$phase|$title"
done <<< "$issues"
fi
done
}
# Set triage phase for an issue (removes old triage phase, adds new)
set_triage_phase() {
local issue_num=$1
local phase=$2
# Remove all existing triage phase labels
local existing_labels
existing_labels=$(gh issue view "$issue_num" --json labels -q '.labels[].name' 2>/dev/null | grep "^auto-dev:triage:" || true)
for old_label in $existing_labels; do
gh issue edit "$issue_num" --remove-label "$old_label" >/dev/null 2>&1 || true
done
# Add new triage phase label
gh issue edit "$issue_num" --add-label "auto-dev:triage:$phase" >/dev/null 2>&1 || true
log "Triage phase → ${MAGENTA}$phase${NC} for issue #$issue_num"
}
# Get current triage phase of an issue
get_triage_phase() {
local issue_num=$1
local labels
labels=$(gh issue view "$issue_num" --json labels -q '.labels[].name' 2>/dev/null || echo "")
for phase in pending analyzing complete blocked; do
if echo "$labels" | grep -q "^auto-dev:triage:$phase$"; then
echo "$phase"
return 0
fi
done
echo ""
}
# Triage a single user-feedback issue
# Analyzes scope, creates atomic child issues, returns recommendation
triage_feedback_issue() {
local issue_num=$1
log "Triaging user feedback issue #$issue_num..."
set_triage_phase "$issue_num" "analyzing"
local session_start
session_start=$(date +%s)
# Get issue details
local issue_json
issue_json=$(gh issue view "$issue_num" --json title,body,comments 2>/dev/null) || issue_json="{}"
local issue_title issue_body
issue_title=$(echo "$issue_json" | jq -r '.title // "Unknown"' 2>/dev/null) || issue_title="Unknown"
issue_body=$(echo "$issue_json" | jq -r '.body // ""' 2>/dev/null) || issue_body=""
local raw_output
raw_output=$(run_claude "$(cat <<'PROMPT'
You are triaging a user feedback issue for the habits/fitstreak project.
## Issue #$issue_num: $issue_title
$issue_body
## Your Task
Analyze this feedback and determine:
1. **Scope Assessment**: Is this small (1 atomic issue), medium (2-4 issues), or epic (5+ issues)?
2. **Actionability**: Can we act on this feedback, or does it need clarification?
## Actions Based on Analysis
### If feedback is CLEAR and ACTIONABLE:
**IMPORTANT: Check for duplicates FIRST!**
Before creating any issue, search for existing issues that might already cover the same work:
\`\`\`bash
gh issue list --state open --json number,title --limit 100
\`\`\`
For each issue you would create:
1. Check if a similar issue already exists (same feature/fix, even if worded differently)
2. If duplicate exists: Skip creation, note the existing issue number
3. If no duplicate: Create the new issue
Create atomic child issues using:
\`\`\`bash
gh issue create --title \"Title here\" --body \"Body here\" --label \"priority-label\"
\`\`\`
Each child issue should:
- Be small and well-defined (completable in one development session)
- Have a clear title starting with a verb (Add, Fix, Update, Implement, etc.)
- Reference the parent: 'Part of #$issue_num'
- Have appropriate priority label (P0-foundation, P1-core, P2-enhancement, P3-future)
- NOT duplicate an existing open issue
### If feedback NEEDS CLARIFICATION:
Add a comment asking for specifics:
\`\`\`bash
gh issue comment $issue_num --body \"Thanks for the feedback! To help us prioritize, could you clarify: [specific questions]\"
\`\`\`
## Output Format
After your analysis and actions, output ONLY this JSON (no markdown, no explanation):
{
\"scope\": \"small|medium|epic|unclear\",
\"issues_created\": [123, 124, 125],
\"issues_linked\": [42, 43],
\"recommendation\": \"close|convert_to_epic|needs_clarification\",
\"summary\": \"Brief summary of what was done\"
}
Where:
- 'issues_created': New issues you created
- 'issues_linked': Existing issues that already cover part of this feedback (duplicates you found)
- 'close': Feedback fully addressed by child issues or existing issues, can close parent
- 'convert_to_epic': Large scope, rename to 'Epic: ...' and keep open as tracker
- 'needs_clarification': Asked user for more info, pause triage
PROMPT
)")
local session_end
session_end=$(date +%s)
# Extract JSON result
local triage_result
triage_result=$(echo "$raw_output" | grep -E '^\{.*\}$' | tail -1) || true
if [ -z "$triage_result" ]; then
# Try to find JSON anywhere in output
triage_result=$(echo "$raw_output" | tr '\n' ' ' | grep -oE '\{[^{}]*"recommendation"[^{}]*\}' | head -1) || true
fi
if [ -z "$triage_result" ]; then
warn "Could not extract triage result JSON"
set_triage_phase "$issue_num" "blocked"
return 1
fi
# Post session memory
local summary
summary=$(echo "$triage_result" | jq -r '.summary // "Triage completed"' 2>/dev/null) || summary="Triage completed"
post_session_memory "$issue_num" "Triage Analysis" "$session_start" "$session_end" "${SESSION_COST:-0}" "$summary"
# Store result for complete_triage
echo "$triage_result"
}
# Complete the triage based on recommendation
complete_triage() {
local issue_num=$1
local triage_result=$2
local recommendation scope issues_created issues_linked summary
recommendation=$(echo "$triage_result" | jq -r '.recommendation // "close"' 2>/dev/null) || recommendation="close"
scope=$(echo "$triage_result" | jq -r '.scope // "small"' 2>/dev/null) || scope="small"
issues_created=$(echo "$triage_result" | jq -r '.issues_created // [] | join(", ")' 2>/dev/null) || issues_created=""
issues_linked=$(echo "$triage_result" | jq -r '.issues_linked // [] | join(", ")' 2>/dev/null) || issues_linked=""
summary=$(echo "$triage_result" | jq -r '.summary // ""' 2>/dev/null) || summary=""
case "$recommendation" in
"close")
log "Closing feedback issue #$issue_num (fully triaged)"
set_triage_phase "$issue_num" "complete"
local close_comment="## ✅ Triage Complete
This feedback has been broken down into actionable issues:
$( [ -n "$issues_created" ] && echo "- Created issues: #${issues_created//,/, #}" || echo "- No new issues created" )
$( [ -n "$issues_linked" ] && echo "- Linked to existing issues: #${issues_linked//,/, #}" || echo "" )
**Scope:** $scope
**Summary:** $summary
Closing this feedback issue as the work is now tracked in the issues above.
---
<sub>🤖 Automated by auto-dev triage</sub>"
gh issue comment "$issue_num" --body "$close_comment" >/dev/null 2>&1 || true
gh issue close "$issue_num" >/dev/null 2>&1 || true
success "Feedback #$issue_num triaged and closed"
;;
"convert_to_epic")
log "Converting feedback #$issue_num to Epic"
set_triage_phase "$issue_num" "complete"
# Get current title and prepend "Epic: " if not already there
local current_title
current_title=$(gh issue view "$issue_num" --json title -q '.title' 2>/dev/null) || current_title=""
if [[ ! "$current_title" =~ ^Epic: ]]; then
gh issue edit "$issue_num" --title "Epic: $current_title" >/dev/null 2>&1 || true
fi
local epic_comment="## 📋 Converted to Epic
This feedback has been analyzed and broken down:
$( [ -n "$issues_created" ] && echo "- Created issues: #${issues_created//,/, #}" || echo "- No new issues created" )
$( [ -n "$issues_linked" ] && echo "- Linked to existing issues: #${issues_linked//,/, #}" || echo "" )
**Scope:** $scope (epic-level)
**Summary:** $summary
This issue will remain open as a tracking epic for the child issues.
---
<sub>🤖 Automated by auto-dev triage</sub>"
gh issue comment "$issue_num" --body "$epic_comment" >/dev/null 2>&1 || true
success "Feedback #$issue_num converted to Epic"
;;
"needs_clarification")
log "Feedback #$issue_num needs clarification from user"
# Remove triage labels - will be re-triaged when user responds
set_triage_phase "$issue_num" "pending"
# Remove user-feedback label temporarily to avoid re-processing
# User can re-add it after providing clarification
warn "Issue #$issue_num paused - waiting for user clarification"
;;
*)
warn "Unknown triage recommendation: $recommendation"
set_triage_phase "$issue_num" "blocked"
;;
esac
}
# Run the triage session for all untriaged feedback
run_triage_session() {
header "SESSION 0: User Feedback Triage"
# Ensure triage labels exist before we try to use them
# (handles case where --init was never run)
ensure_labels_exist
# Collect ALL issues that need triage:
# 1. New untriaged feedback (no triage labels)
# 2. Issues stuck in pending/analyzing from previous runs
local new_issues in_progress_issues all_triage_issues=""
new_issues=$(find_untriaged_feedback_issues)
in_progress_issues=$(find_triage_in_progress)
# Combine lists (new issues first, then in-progress)
if [ -n "$new_issues" ]; then
# Format: issue_num|title
all_triage_issues="$new_issues"
fi
if [ -n "$in_progress_issues" ]; then
# in_progress format is: issue_num|triage:phase|title
# Convert to: issue_num|title for consistent processing
local converted_in_progress
converted_in_progress=$(echo "$in_progress_issues" | while IFS='|' read -r num phase title; do
[ -z "$num" ] && continue
echo "$num|$title"
done)
if [ -n "$converted_in_progress" ]; then
if [ -n "$all_triage_issues" ]; then
all_triage_issues="$all_triage_issues"$'\n'"$converted_in_progress"
else
all_triage_issues="$converted_in_progress"
fi
fi
fi
if [ -z "$all_triage_issues" ]; then
log "No user-feedback issues to triage"
return 0
fi
local count new_count in_progress_count
count=$(echo "$all_triage_issues" | wc -l | tr -d ' ')
new_count=$([ -n "$new_issues" ] && echo "$new_issues" | wc -l | tr -d ' ' || echo "0")
in_progress_count=$([ -n "$in_progress_issues" ] && echo "$in_progress_issues" | wc -l | tr -d ' ' || echo "0")
log "Found $count user-feedback issue(s) to triage ($new_count new, $in_progress_count resuming)"
# Process each feedback issue
# Note: Using fd 3 to avoid stdin consumption by commands inside the loop
# (run_claude and other commands may read from stdin, consuming remaining issues)
while IFS='|' read -r -u 3 issue_num issue_title; do
[ -z "$issue_num" ] && continue
log "Processing feedback: #$issue_num - $issue_title"
local triage_result
if triage_result=$(triage_feedback_issue "$issue_num"); then
complete_triage "$issue_num" "$triage_result"
else
warn "Triage failed for issue #$issue_num"
fi
# Brief pause between issues
sleep 2
done 3<<< "$all_triage_issues"
success "Triage session complete"
}
#═══════════════════════════════════════════════════════════════════════════════
# STREAMING OUTPUT FORMATTER
#═══════════════════════════════════════════════════════════════════════════════
# Track session metrics
SESSION_START_TIME=""
SESSION_COST=""
# Format streaming JSON to show progress to human orchestrator
# Streams: text responses, tool calls, and captures final result
# IMPORTANT: Only outputs clean text to stdout, never raw JSON
# Press ESC to pause, any key to resume (only in interactive sessions)
format_progress() {
local line type subtype final_result=""
# Tool name mapping stored in temp files (for subshell access)
# Use mktemp for safer temp file creation (avoids PID conflicts)