claude-queue 1.3.3 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,954 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # claude-queue - Automated GitHub issue solver & creator
4
+ #
5
+ # Commands:
6
+ # claude-queue [options] Solve open issues (default)
7
+ # claude-queue create [options] Create issues from text or interactively
8
+ #
9
+ # Solve options:
10
+ # --max-retries N Max retries per issue (default: 3)
11
+ # --max-turns N Max Claude turns per attempt (default: 50)
12
+ # --label LABEL Only process issues with this label
13
+ # --model MODEL Claude model to use
14
+ # -v, --version Show version
15
+ # -h, --help Show this help message
16
+ #
17
+ # Create options:
18
+ # -i, --interactive Interview mode (Claude asks questions)
19
+ # --label LABEL Add this label to all created issues
20
+ # --model MODEL Claude model to use
21
+ # -h, --help Show help for create
22
+
23
+ set -euo pipefail
24
+
25
+ VERSION=$(node -p "require('$(dirname "$0")/package.json').version" 2>/dev/null || echo "unknown")
26
+
27
+ MAX_RETRIES=3
28
+ MAX_TURNS=50
29
+ ISSUE_FILTER=""
30
+ MODEL_FLAG=""
31
+ DATE=$(date +%Y-%m-%d)
32
+ TIMESTAMP=$(date +%H%M%S)
33
+ BRANCH="claude-queue/${DATE}"
34
+ LOG_DIR="/tmp/claude-queue-${DATE}-${TIMESTAMP}"
35
+
36
+ LABEL_PROGRESS="claude-queue:in-progress"
37
+ LABEL_SOLVED="claude-queue:solved"
38
+ LABEL_FAILED="claude-queue:failed"
39
+
40
+ RED='\033[0;31m'
41
+ GREEN='\033[0;32m'
42
+ YELLOW='\033[1;33m'
43
+ BLUE='\033[0;34m'
44
+ DIM='\033[2m'
45
+ BOLD='\033[1m'
46
+ NC='\033[0m'
47
+
48
+ declare -a SOLVED_ISSUES=()
49
+ declare -a FAILED_ISSUES=()
50
+ declare -a SKIPPED_ISSUES=()
51
+ CURRENT_ISSUE=""
52
+ START_TIME=$(date +%s)
53
+
54
+ show_help() {
55
+ echo "claude-queue v${VERSION} — Automated GitHub issue solver & creator"
56
+ echo ""
57
+ echo "Usage:"
58
+ echo " claude-queue [options] Solve open issues (default)"
59
+ echo " claude-queue create [options] [text] Create issues from text or interactively"
60
+ echo ""
61
+ echo "Solve options:"
62
+ echo " --max-retries N Max retries per issue (default: 3)"
63
+ echo " --max-turns N Max Claude turns per attempt (default: 50)"
64
+ echo " --label LABEL Only process issues with this label"
65
+ echo " --model MODEL Claude model to use"
66
+ echo " -v, --version Show version"
67
+ echo " -h, --help Show this help message"
68
+ echo ""
69
+ echo "Run 'claude-queue create --help' for create options."
70
+ }
71
+
72
+ show_create_help() {
73
+ echo "claude-queue create — Generate GitHub issues from text or an interactive interview"
74
+ echo ""
75
+ echo "Usage:"
76
+ echo " claude-queue create \"description\" Create issues from inline text"
77
+ echo " claude-queue create Prompt for text input (Ctrl+D to finish)"
78
+ echo " claude-queue create -i Interactive interview mode"
79
+ echo ""
80
+ echo "Options:"
81
+ echo " -i, --interactive Interview mode (Claude asks clarifying questions first)"
82
+ echo " --label LABEL Add this label to every created issue"
83
+ echo " --model MODEL Claude model to use"
84
+ echo " -h, --help Show this help message"
85
+ }
86
+
87
+ log() { echo -e "${DIM}$(date +%H:%M:%S)${NC} ${BLUE}[claude-queue]${NC} $1"; }
88
+ log_success() { echo -e "${DIM}$(date +%H:%M:%S)${NC} ${GREEN}[claude-queue]${NC} $1"; }
89
+ log_warn() { echo -e "${DIM}$(date +%H:%M:%S)${NC} ${YELLOW}[claude-queue]${NC} $1"; }
90
+ log_error() { echo -e "${DIM}$(date +%H:%M:%S)${NC} ${RED}[claude-queue]${NC} $1"; }
91
+ log_header() { echo -e "\n${BOLD}═══ $1 ═══${NC}\n"; }
92
+
93
+ cleanup() {
94
+ local exit_code=$?
95
+
96
+ if [ -n "$CURRENT_ISSUE" ]; then
97
+ log_warn "Interrupted while working on issue #${CURRENT_ISSUE}"
98
+ gh issue edit "$CURRENT_ISSUE" --remove-label "$LABEL_PROGRESS" 2>/dev/null || true
99
+ gh issue edit "$CURRENT_ISSUE" --add-label "$LABEL_FAILED" 2>/dev/null || true
100
+ fi
101
+
102
+ if [ $exit_code -ne 0 ] && [ ${#SOLVED_ISSUES[@]} -gt 0 ]; then
103
+ log_warn "Script interrupted but ${#SOLVED_ISSUES[@]} issue(s) were solved."
104
+ log_warn "Branch '${BRANCH}' has your commits. Push manually if needed."
105
+ fi
106
+
107
+ if [ -d "$LOG_DIR" ]; then
108
+ log "Logs saved to: ${LOG_DIR}"
109
+ fi
110
+ }
111
+ trap cleanup EXIT
112
+
113
+ preflight() {
114
+ log_header "Preflight Checks"
115
+
116
+ local failed=false
117
+
118
+ for cmd in gh claude git jq; do
119
+ if command -v "$cmd" &>/dev/null; then
120
+ log " $cmd ... found"
121
+ else
122
+ log_error " $cmd ... NOT FOUND"
123
+ failed=true
124
+ fi
125
+ done
126
+
127
+ if ! gh auth status &>/dev/null; then
128
+ log_error " gh auth ... not authenticated"
129
+ failed=true
130
+ else
131
+ log " gh auth ... ok"
132
+ fi
133
+
134
+ if ! git rev-parse --is-inside-work-tree &>/dev/null; then
135
+ log_error " git repo ... not inside a git repository"
136
+ failed=true
137
+ else
138
+ log " git repo ... ok"
139
+ fi
140
+
141
+ if [ -n "$(git status --porcelain 2>/dev/null)" ]; then
142
+ log_error " working tree ... dirty (commit or stash changes first)"
143
+ failed=true
144
+ else
145
+ log " working tree ... clean"
146
+ fi
147
+
148
+ if [ "$failed" = true ]; then
149
+ log_error "Preflight failed. Aborting."
150
+ exit 1
151
+ fi
152
+
153
+ mkdir -p "$LOG_DIR"
154
+ log " log dir ... ${LOG_DIR}"
155
+ }
156
+
157
+ ensure_labels() {
158
+ log "Creating labels (if missing)..."
159
+
160
+ gh label create "$LABEL_PROGRESS" --color "fbca04" --description "claude-queue is working on this" --force 2>/dev/null || true
161
+ gh label create "$LABEL_SOLVED" --color "0e8a16" --description "Solved by claude-queue" --force 2>/dev/null || true
162
+ gh label create "$LABEL_FAILED" --color "d93f0b" --description "claude-queue could not solve this" --force 2>/dev/null || true
163
+ }
164
+
165
+ setup_branch() {
166
+ log_header "Branch Setup"
167
+
168
+ local default_branch
169
+ default_branch=$(gh repo view --json defaultBranchRef -q '.defaultBranchRef.name')
170
+ log "Default branch: ${default_branch}"
171
+
172
+ git fetch origin "$default_branch" --quiet
173
+
174
+ if git show-ref --verify --quiet "refs/heads/${BRANCH}"; then
175
+ log_warn "Branch ${BRANCH} already exists, adding timestamp suffix"
176
+ BRANCH="${BRANCH}-${TIMESTAMP}"
177
+ fi
178
+
179
+ git checkout -b "$BRANCH" "origin/${default_branch}" --quiet
180
+ log_success "Created branch: ${BRANCH}"
181
+ }
182
+
183
+ fetch_issues() {
184
+ local args=(--state open --json "number,title,body,labels" --limit 200)
185
+
186
+ if [ -n "$ISSUE_FILTER" ]; then
187
+ args+=(--label "$ISSUE_FILTER")
188
+ fi
189
+
190
+ gh issue list "${args[@]}"
191
+ }
192
+
193
+ process_issue() {
194
+ local issue_number=$1
195
+ local issue_title="$2"
196
+ local attempt=0
197
+ local solved=false
198
+ local issue_log="${LOG_DIR}/issue-${issue_number}.md"
199
+ local checkpoint
200
+ checkpoint=$(git rev-parse HEAD)
201
+
202
+ CURRENT_ISSUE="$issue_number"
203
+
204
+ log_header "Issue #${issue_number}: ${issue_title}"
205
+
206
+ gh issue edit "$issue_number" \
207
+ --remove-label "$LABEL_SOLVED" \
208
+ --remove-label "$LABEL_FAILED" \
209
+ 2>/dev/null || true
210
+ gh issue edit "$issue_number" --add-label "$LABEL_PROGRESS"
211
+
212
+ {
213
+ echo "# Issue #${issue_number}: ${issue_title}"
214
+ echo ""
215
+ echo "**Started:** $(date)"
216
+ echo ""
217
+ } > "$issue_log"
218
+
219
+ while [ "$attempt" -lt "$MAX_RETRIES" ] && [ "$solved" = false ]; do
220
+ attempt=$((attempt + 1))
221
+ log "Attempt ${attempt}/${MAX_RETRIES}"
222
+
223
+ git reset --hard "$checkpoint" --quiet 2>/dev/null || true
224
+ git clean -fd --quiet 2>/dev/null || true
225
+
226
+ echo "## Attempt ${attempt}" >> "$issue_log"
227
+ echo "" >> "$issue_log"
228
+
229
+ local custom_instructions=""
230
+ if [ -f ".claude-queue" ]; then
231
+ custom_instructions="
232
+
233
+ Additional project-specific instructions:
234
+ $(cat .claude-queue)"
235
+ fi
236
+
237
+ local prompt
238
+ prompt="You are an automated assistant solving a GitHub issue in this repository.
239
+
240
+ First, read the full issue details by running:
241
+ gh issue view ${issue_number}
242
+
243
+ Then:
244
+ 1. Explore the codebase to understand the project structure and conventions
245
+ 2. Implement a complete, correct fix for the issue
246
+ 3. Run any existing tests to verify your fix doesn't break anything
247
+ 4. If tests fail because of your changes, fix them
248
+
249
+ Rules:
250
+ - Do NOT create any git commits
251
+ - Do NOT push anything
252
+ - Match the existing code style exactly
253
+ - Only change what is necessary to solve the issue
254
+ ${custom_instructions}
255
+ If this issue does NOT require code changes (e.g. it's a question, a request for external action,
256
+ a finding, or something that can't be solved with code), output a line that says CLAUDE_QUEUE_NO_CODE
257
+ followed by an explanation of what needs to be done instead.
258
+
259
+ Otherwise, when you are done, output a line that says CLAUDE_QUEUE_SUMMARY followed by a 2-3 sentence
260
+ description of what you changed and why."
261
+
262
+ local attempt_log="${LOG_DIR}/issue-${issue_number}-attempt-${attempt}.log"
263
+ local claude_exit=0
264
+
265
+ # shellcheck disable=SC2086
266
+ claude -p "$prompt" \
267
+ --dangerously-skip-permissions \
268
+ --max-turns "$MAX_TURNS" \
269
+ $MODEL_FLAG \
270
+ > "$attempt_log" 2>&1 || claude_exit=$?
271
+
272
+ if [ "$claude_exit" -ne 0 ]; then
273
+ log_warn "Claude exited with code ${claude_exit}"
274
+ echo "**Claude exited with code ${claude_exit}**" >> "$issue_log"
275
+ echo "" >> "$issue_log"
276
+ continue
277
+ fi
278
+
279
+ local no_code_reason
280
+ no_code_reason=$(grep -A 20 "CLAUDE_QUEUE_NO_CODE" "$attempt_log" 2>/dev/null | tail -n +2 | head -10 || echo "")
281
+
282
+ if [ -n "$no_code_reason" ]; then
283
+ log "Issue does not require code changes"
284
+ {
285
+ echo "### No Code Changes Required"
286
+ echo "$no_code_reason"
287
+ echo ""
288
+ } >> "$issue_log"
289
+ solved=true
290
+ log_success "Issue #${issue_number} handled (no code changes needed)"
291
+ break
292
+ fi
293
+
294
+ local changed_files
295
+ changed_files=$(git diff --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
296
+
297
+ if [ -z "$changed_files" ]; then
298
+ log_warn "No file changes detected"
299
+ echo "**No file changes detected**" >> "$issue_log"
300
+ echo "" >> "$issue_log"
301
+ continue
302
+ fi
303
+
304
+ log_success "Changes detected in:"
305
+ echo "$changed_files" | while IFS= read -r f; do
306
+ log " ${f}"
307
+ done
308
+
309
+ local summary
310
+ summary=$(grep -A 20 "CLAUDE_QUEUE_SUMMARY" "$attempt_log" 2>/dev/null | tail -n +2 | head -10 || echo "No summary provided.")
311
+
312
+ {
313
+ echo "### Summary"
314
+ echo "$summary"
315
+ echo ""
316
+ echo "### Changed Files"
317
+ echo "$changed_files" | while IFS= read -r f; do echo "- \`${f}\`"; done
318
+ echo ""
319
+ } >> "$issue_log"
320
+
321
+ git add -A
322
+ git commit -m "fix: resolve #${issue_number} - ${issue_title}
323
+
324
+ Automated fix by claude-queue.
325
+ Closes #${issue_number}" --quiet
326
+
327
+ solved=true
328
+
329
+ log_success "Solved issue #${issue_number} on attempt ${attempt}"
330
+ done
331
+
332
+ gh issue edit "$issue_number" --remove-label "$LABEL_PROGRESS" 2>/dev/null || true
333
+
334
+ {
335
+ echo "**Finished:** $(date)"
336
+ echo "**Status:** $([ "$solved" = true ] && echo "SOLVED" || echo "FAILED after ${MAX_RETRIES} attempts")"
337
+ } >> "$issue_log"
338
+
339
+ if [ "$solved" = true ]; then
340
+ gh issue edit "$issue_number" --add-label "$LABEL_SOLVED"
341
+ gh issue comment "$issue_number" --body-file "$issue_log" 2>/dev/null || true
342
+ SOLVED_ISSUES+=("${issue_number}|${issue_title}")
343
+ else
344
+ gh issue edit "$issue_number" --add-label "$LABEL_FAILED"
345
+ gh issue comment "$issue_number" --body "claude-queue failed to solve this issue after ${MAX_RETRIES} attempts." 2>/dev/null || true
346
+ FAILED_ISSUES+=("${issue_number}|${issue_title}")
347
+ git reset --hard "$checkpoint" --quiet 2>/dev/null || true
348
+ git clean -fd --quiet 2>/dev/null || true
349
+ fi
350
+
351
+ CURRENT_ISSUE=""
352
+ }
353
+
354
+ review_and_fix() {
355
+ log_header "Final Review & Fix Pass"
356
+
357
+ local review_log="${LOG_DIR}/review.md"
358
+
359
+ local prompt
360
+ prompt="You are doing a final review pass on automated code changes in this repository.
361
+
362
+ Look at all uncommitted and recently committed changes on this branch. For each file that was modified:
363
+ 1. Read the full file
364
+ 2. Check for bugs, incomplete implementations, lazy code, missed edge cases, or style inconsistencies
365
+ 3. Fix anything you find
366
+
367
+ Rules:
368
+ - Do NOT create any git commits
369
+ - Do NOT push anything
370
+ - Only fix real problems, don't refactor for style preferences
371
+ - Match the existing code style exactly
372
+
373
+ When you are done, output a line that says CLAUDE_QUEUE_REVIEW followed by a brief summary of what you fixed. If nothing needed fixing, say so."
374
+
375
+ # shellcheck disable=SC2086
376
+ claude -p "$prompt" \
377
+ --dangerously-skip-permissions \
378
+ --max-turns "$MAX_TURNS" \
379
+ $MODEL_FLAG \
380
+ > "$review_log" 2>&1 || true
381
+
382
+ local changed_files
383
+ changed_files=$(git diff --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
384
+
385
+ if [ -n "$changed_files" ]; then
386
+ log_success "Review pass made fixes:"
387
+ echo "$changed_files" | while IFS= read -r f; do
388
+ log " ${f}"
389
+ done
390
+
391
+ git add -A
392
+ git commit -m "chore: final review pass
393
+
394
+ Automated review and fixes by claude-queue." --quiet
395
+ else
396
+ log "Review pass found nothing to fix"
397
+ fi
398
+ }
399
+
400
+ create_pr() {
401
+ log_header "Creating Pull Request"
402
+
403
+ local default_branch
404
+ default_branch=$(gh repo view --json defaultBranchRef -q '.defaultBranchRef.name')
405
+ local elapsed=$(( $(date +%s) - START_TIME ))
406
+ local duration
407
+ duration="$(( elapsed / 3600 ))h $(( (elapsed % 3600) / 60 ))m $(( elapsed % 60 ))s"
408
+ local pr_body="${LOG_DIR}/pr-body.md"
409
+ local total_processed=$(( ${#SOLVED_ISSUES[@]} + ${#FAILED_ISSUES[@]} ))
410
+
411
+ {
412
+ echo "## claude-queue Run Summary"
413
+ echo ""
414
+ echo "| Metric | Value |"
415
+ echo "|--------|-------|"
416
+ echo "| Date | ${DATE} |"
417
+ echo "| Duration | ${duration} |"
418
+ echo "| Issues processed | ${total_processed} |"
419
+ echo "| Solved | ${#SOLVED_ISSUES[@]} |"
420
+ echo "| Failed | ${#FAILED_ISSUES[@]} |"
421
+ echo "| Skipped | ${#SKIPPED_ISSUES[@]} |"
422
+ echo ""
423
+
424
+ if [ ${#SOLVED_ISSUES[@]} -gt 0 ]; then
425
+ echo "### Solved Issues"
426
+ echo ""
427
+ echo "| Issue | Title |"
428
+ echo "|-------|-------|"
429
+ for entry in "${SOLVED_ISSUES[@]}"; do
430
+ local num="${entry%%|*}"
431
+ local title="${entry#*|}"
432
+ echo "| #${num} | ${title} |"
433
+ done
434
+ echo ""
435
+ fi
436
+
437
+ if [ ${#FAILED_ISSUES[@]} -gt 0 ]; then
438
+ echo "### Failed Issues"
439
+ echo ""
440
+ echo "| Issue | Title |"
441
+ echo "|-------|-------|"
442
+ for entry in "${FAILED_ISSUES[@]}"; do
443
+ local num="${entry%%|*}"
444
+ local title="${entry#*|}"
445
+ echo "| #${num} | ${title} |"
446
+ done
447
+ echo ""
448
+ fi
449
+
450
+ echo "---"
451
+ echo ""
452
+ echo "### Chain Logs"
453
+ echo ""
454
+
455
+ for log_file in "${LOG_DIR}"/issue-*.md; do
456
+ if [ ! -f "$log_file" ]; then
457
+ continue
458
+ fi
459
+
460
+ local issue_num
461
+ issue_num=$(basename "$log_file" | grep -oE '[0-9]+')
462
+
463
+ echo "<details>"
464
+ echo "<summary>Issue #${issue_num} Log</summary>"
465
+ echo ""
466
+ head -c 40000 "$log_file"
467
+ echo ""
468
+ echo "</details>"
469
+ echo ""
470
+ done
471
+ } > "$pr_body"
472
+
473
+ local body_size
474
+ body_size=$(wc -c < "$pr_body")
475
+ if [ "$body_size" -gt 60000 ]; then
476
+ log_warn "PR body is ${body_size} bytes, truncating to fit GitHub limits"
477
+ head -c 59000 "$pr_body" > "${pr_body}.tmp"
478
+ {
479
+ echo ""
480
+ echo ""
481
+ echo "---"
482
+ echo "*Log truncated. Full logs available at: ${LOG_DIR}*"
483
+ } >> "${pr_body}.tmp"
484
+ mv "${pr_body}.tmp" "$pr_body"
485
+ fi
486
+
487
+ git push origin "$BRANCH" --quiet
488
+ log_success "Pushed branch to origin"
489
+
490
+ local pr_url
491
+ pr_url=$(gh pr create \
492
+ --base "$default_branch" \
493
+ --head "$BRANCH" \
494
+ --title "claude-queue: Automated fixes (${DATE})" \
495
+ --body-file "$pr_body")
496
+
497
+ log_success "Pull request created: ${pr_url}"
498
+ }
499
+
500
+ main() {
501
+ echo -e "${BOLD}"
502
+ echo ' _ _ '
503
+ echo ' ___| | __ _ _ _ __| | ___ __ _ _ _ ___ _ _ ___'
504
+ echo ' / __| |/ _` | | | |/ _` |/ _ \_____ / _` | | | |/ _ \ | | |/ _ \'
505
+ echo ' | (__| | (_| | |_| | (_| | __/_____| (_| | |_| | __/ |_| | __/'
506
+ echo ' \___|_|\__,_|\__,_|\__,_|\___| \__, |\__,_|\___|\__,_|\___|'
507
+ echo ' |_| '
508
+ echo -e "${NC}"
509
+ echo -e " ${DIM}Automated GitHub issue solver${NC}"
510
+ echo ""
511
+
512
+ preflight
513
+ ensure_labels
514
+ setup_branch
515
+
516
+ log_header "Fetching Issues"
517
+
518
+ local issues
519
+ issues=$(fetch_issues)
520
+ local total
521
+ total=$(echo "$issues" | jq length)
522
+
523
+ if [ "$total" -eq 0 ]; then
524
+ log "No open issues found. Going back to sleep."
525
+ exit 0
526
+ fi
527
+
528
+ log "Found ${total} open issue(s)"
529
+
530
+ for i in $(seq 0 $((total - 1))); do
531
+ local number title labels
532
+ number=$(echo "$issues" | jq -r ".[$i].number")
533
+ title=$(echo "$issues" | jq -r ".[$i].title")
534
+ labels=$(echo "$issues" | jq -r "[.[$i].labels[].name] | join(\",\")" 2>/dev/null || echo "")
535
+
536
+ if echo "$labels" | grep -q "claude-queue:"; then
537
+ log "Skipping #${number} (already has a claude-queue label)"
538
+ SKIPPED_ISSUES+=("${number}|${title}")
539
+ continue
540
+ fi
541
+
542
+ process_issue "$number" "$title" || true
543
+ done
544
+
545
+ if [ ${#SOLVED_ISSUES[@]} -gt 0 ]; then
546
+ review_and_fix
547
+ create_pr
548
+ else
549
+ log_warn "No issues were solved. No PR created."
550
+ fi
551
+
552
+ log_header "claude-queue Complete"
553
+
554
+ local elapsed=$(( $(date +%s) - START_TIME ))
555
+ log "Duration: $(( elapsed / 3600 ))h $(( (elapsed % 3600) / 60 ))m $(( elapsed % 60 ))s"
556
+ log_success "Solved: ${#SOLVED_ISSUES[@]}"
557
+ if [ ${#FAILED_ISSUES[@]} -gt 0 ]; then
558
+ log_error "Failed: ${#FAILED_ISSUES[@]}"
559
+ fi
560
+ if [ ${#SKIPPED_ISSUES[@]} -gt 0 ]; then
561
+ log_warn "Skipped: ${#SKIPPED_ISSUES[@]}"
562
+ fi
563
+ log "Logs: ${LOG_DIR}"
564
+ }
565
+
566
+ create_preflight() {
567
+ log_header "Preflight Checks"
568
+
569
+ local failed=false
570
+
571
+ for cmd in gh claude jq; do
572
+ if command -v "$cmd" &>/dev/null; then
573
+ log " $cmd ... found"
574
+ else
575
+ log_error " $cmd ... NOT FOUND"
576
+ failed=true
577
+ fi
578
+ done
579
+
580
+ if ! gh auth status &>/dev/null; then
581
+ log_error " gh auth ... not authenticated"
582
+ failed=true
583
+ else
584
+ log " gh auth ... ok"
585
+ fi
586
+
587
+ if ! git rev-parse --is-inside-work-tree &>/dev/null; then
588
+ log_error " git repo ... not inside a git repository"
589
+ failed=true
590
+ else
591
+ log " git repo ... ok"
592
+ fi
593
+
594
+ if [ "$failed" = true ]; then
595
+ log_error "Preflight failed. Aborting."
596
+ exit 1
597
+ fi
598
+ }
599
+
600
+ get_repo_labels() {
601
+ gh label list --json name -q '.[].name' 2>/dev/null | paste -sd ',' -
602
+ }
603
+
604
+ extract_json() {
605
+ local input="$1"
606
+ local json
607
+
608
+ json=$(echo "$input" | sed -n '/^```\(json\)\?$/,/^```$/{ /^```/d; p; }')
609
+ if [ -z "$json" ]; then
610
+ json="$input"
611
+ fi
612
+
613
+ if echo "$json" | jq empty 2>/dev/null; then
614
+ echo "$json"
615
+ return 0
616
+ fi
617
+
618
+ json=$(echo "$input" | grep -o '\[.*\]' | head -1)
619
+ if [ -n "$json" ] && echo "$json" | jq empty 2>/dev/null; then
620
+ echo "$json"
621
+ return 0
622
+ fi
623
+
624
+ return 1
625
+ }
626
+
627
+ create_from_text() {
628
+ local user_text="$1"
629
+ local repo_labels
630
+ repo_labels=$(get_repo_labels)
631
+
632
+ log "Analyzing text and generating issues..."
633
+
634
+ local prompt
635
+ prompt="You are a GitHub issue planner. The user wants to create issues for a repository.
636
+
637
+ Existing labels in the repo: ${repo_labels}
638
+
639
+ The user's description:
640
+ ${user_text}
641
+
642
+ Decompose this into a JSON array of well-structured GitHub issues. Each issue should have:
643
+ - \"title\": a clear, concise issue title
644
+ - \"body\": a detailed issue body in markdown (include acceptance criteria where appropriate)
645
+ - \"labels\": an array of label strings (reuse existing repo labels when they fit, or suggest new ones)
646
+
647
+ Rules:
648
+ - Create separate issues for logically distinct tasks
649
+ - Each issue should be independently actionable
650
+ - Use clear, imperative titles (e.g. \"Add dark mode toggle to settings page\")
651
+ - If the description is vague, make reasonable assumptions and note them in the body
652
+
653
+ Output ONLY the JSON array, no other text."
654
+
655
+ local output
656
+ # shellcheck disable=SC2086
657
+ output=$(claude -p "$prompt" $MODEL_FLAG 2>/dev/null)
658
+
659
+ local json
660
+ if ! json=$(extract_json "$output"); then
661
+ log_error "Failed to parse Claude's response as JSON"
662
+ log_error "Raw output:"
663
+ echo "$output"
664
+ exit 1
665
+ fi
666
+
667
+ local count
668
+ count=$(echo "$json" | jq length)
669
+ if [ "$count" -eq 0 ]; then
670
+ log_error "No issues were generated"
671
+ exit 1
672
+ fi
673
+
674
+ echo "$json"
675
+ }
676
+
677
+ create_interactive() {
678
+ local repo_labels
679
+ repo_labels=$(get_repo_labels)
680
+ local conversation=""
681
+ local max_turns=10
682
+ local turn=0
683
+
684
+ local system_prompt="You are a GitHub issue planner conducting an interview to understand what issues to create for a repository.
685
+
686
+ Existing labels in the repo: ${repo_labels}
687
+
688
+ Your job:
689
+ 1. Ask focused questions to understand what the user wants to build or fix
690
+ 2. Ask about priorities, scope, and acceptance criteria
691
+ 3. When you have enough information, output the marker CLAUDE_QUEUE_READY on its own line, followed by a JSON array of issues
692
+
693
+ Each issue in the JSON array should have:
694
+ - \"title\": a clear, concise issue title
695
+ - \"body\": a detailed issue body in markdown
696
+ - \"labels\": an array of label strings (reuse existing repo labels when they fit)
697
+
698
+ Rules:
699
+ - Ask one question at a time
700
+ - Keep questions short and specific
701
+ - After 2-3 questions you should have enough context — don't over-interview
702
+ - If the user says \"done\", immediately generate the issues with what you know
703
+ - Output ONLY your question text (no JSON) until you're ready to generate issues
704
+ - When ready, output CLAUDE_QUEUE_READY on its own line followed by ONLY the JSON array"
705
+
706
+ echo -e "${BOLD}Interactive issue creation${NC}"
707
+ echo -e "${DIM}Answer Claude's questions. Type 'done' to generate issues at any time.${NC}"
708
+ echo ""
709
+
710
+ while [ "$turn" -lt "$max_turns" ]; do
711
+ turn=$((turn + 1))
712
+
713
+ local prompt
714
+ if [ -z "$conversation" ]; then
715
+ prompt="${system_prompt}
716
+
717
+ Start by asking your first question."
718
+ else
719
+ prompt="${system_prompt}
720
+
721
+ Conversation so far:
722
+ ${conversation}
723
+
724
+ Continue the interview or, if you have enough information, output CLAUDE_QUEUE_READY followed by the JSON array."
725
+ fi
726
+
727
+ local output
728
+ # shellcheck disable=SC2086
729
+ output=$(claude -p "$prompt" $MODEL_FLAG 2>/dev/null)
730
+
731
+ if echo "$output" | grep -q "CLAUDE_QUEUE_READY"; then
732
+ local json_part
733
+ json_part=$(echo "$output" | sed -n '/CLAUDE_QUEUE_READY/,$ p' | tail -n +2)
734
+
735
+ local json
736
+ if ! json=$(extract_json "$json_part"); then
737
+ log_error "Failed to parse generated issues as JSON"
738
+ exit 1
739
+ fi
740
+
741
+ echo "$json"
742
+ return 0
743
+ fi
744
+
745
+ echo -e "${BLUE}Claude:${NC} ${output}"
746
+ echo ""
747
+
748
+ local user_input
749
+ read -r -p "You: " user_input
750
+
751
+ if [ "$user_input" = "done" ]; then
752
+ conversation="${conversation}
753
+ Claude: ${output}
754
+ User: Please generate the issues now with what you know."
755
+
756
+ local final_prompt="${system_prompt}
757
+
758
+ Conversation so far:
759
+ ${conversation}
760
+
761
+ The user wants you to generate the issues now. Output CLAUDE_QUEUE_READY followed by the JSON array."
762
+
763
+ local final_output
764
+ # shellcheck disable=SC2086
765
+ final_output=$(claude -p "$final_prompt" $MODEL_FLAG 2>/dev/null)
766
+
767
+ local final_json_part
768
+ final_json_part=$(echo "$final_output" | sed -n '/CLAUDE_QUEUE_READY/,$ p' | tail -n +2)
769
+ if [ -z "$final_json_part" ]; then
770
+ final_json_part="$final_output"
771
+ fi
772
+
773
+ local json
774
+ if ! json=$(extract_json "$final_json_part"); then
775
+ log_error "Failed to parse generated issues as JSON"
776
+ exit 1
777
+ fi
778
+
779
+ echo "$json"
780
+ return 0
781
+ fi
782
+
783
+ conversation="${conversation}
784
+ Claude: ${output}
785
+ User: ${user_input}"
786
+ done
787
+
788
+ log_warn "Reached maximum interview turns, generating issues with current information..."
789
+
790
+ local final_prompt="${system_prompt}
791
+
792
+ Conversation so far:
793
+ ${conversation}
794
+
795
+ You've reached the maximum number of questions. Output CLAUDE_QUEUE_READY followed by the JSON array now."
796
+
797
+ local final_output
798
+ # shellcheck disable=SC2086
799
+ final_output=$(claude -p "$final_prompt" $MODEL_FLAG 2>/dev/null)
800
+
801
+ local final_json_part
802
+ final_json_part=$(echo "$final_output" | sed -n '/CLAUDE_QUEUE_READY/,$ p' | tail -n +2)
803
+ if [ -z "$final_json_part" ]; then
804
+ final_json_part="$final_output"
805
+ fi
806
+
807
+ local json
808
+ if ! json=$(extract_json "$final_json_part"); then
809
+ log_error "Failed to parse generated issues as JSON"
810
+ exit 1
811
+ fi
812
+
813
+ echo "$json"
814
+ }
815
+
816
+ preview_issues() {
817
+ local json="$1"
818
+ local count
819
+ count=$(echo "$json" | jq length)
820
+
821
+ echo ""
822
+ echo -e "${BOLD}═══ Issue Preview ═══${NC}"
823
+ echo ""
824
+
825
+ for i in $(seq 0 $((count - 1))); do
826
+ local title labels body
827
+ title=$(echo "$json" | jq -r ".[$i].title")
828
+ labels=$(echo "$json" | jq -r ".[$i].labels // [] | join(\", \")")
829
+ body=$(echo "$json" | jq -r ".[$i].body" | head -3)
830
+
831
+ echo -e " ${BOLD}$((i + 1)). ${title}${NC}"
832
+ if [ -n "$labels" ]; then
833
+ echo -e " ${DIM}Labels: ${labels}${NC}"
834
+ fi
835
+ echo -e " ${DIM}$(echo "$body" | head -1)${NC}"
836
+ echo ""
837
+ done
838
+ }
839
+
840
+ confirm_and_create() {
841
+ local json="$1"
842
+ local extra_label="$2"
843
+ local count
844
+ count=$(echo "$json" | jq length)
845
+
846
+ local prompt_text="Create ${count} issue(s)? [y/N] "
847
+ read -r -p "$prompt_text" confirm
848
+
849
+ if [[ ! "$confirm" =~ ^[Yy]$ ]]; then
850
+ log "Cancelled."
851
+ exit 0
852
+ fi
853
+
854
+ echo ""
855
+
856
+ for i in $(seq 0 $((count - 1))); do
857
+ local title body
858
+ title=$(echo "$json" | jq -r ".[$i].title")
859
+ body=$(echo "$json" | jq -r ".[$i].body")
860
+
861
+ local label_args=()
862
+ local issue_labels
863
+ issue_labels=$(echo "$json" | jq -r ".[$i].labels // [] | .[]")
864
+ while IFS= read -r lbl; do
865
+ if [ -n "$lbl" ]; then
866
+ label_args+=(--label "$lbl")
867
+ fi
868
+ done <<< "$issue_labels"
869
+
870
+ if [ -n "$extra_label" ]; then
871
+ label_args+=(--label "$extra_label")
872
+ fi
873
+
874
+ local issue_url
875
+ issue_url=$(gh issue create --title "$title" --body "$body" "${label_args[@]}" 2>&1)
876
+ log_success "Created: ${issue_url}"
877
+ done
878
+
879
+ echo ""
880
+ log_success "Created ${count} issue(s)"
881
+ }
882
+
883
+ cmd_create() {
884
+ local interactive=false
885
+ local extra_label=""
886
+ local user_text=""
887
+
888
+ while [[ $# -gt 0 ]]; do
889
+ case $1 in
890
+ -i|--interactive) interactive=true; shift ;;
891
+ --label) extra_label="$2"; shift 2 ;;
892
+ --model) MODEL_FLAG="--model $2"; shift 2 ;;
893
+ -h|--help) show_create_help; exit 0 ;;
894
+ -*) echo "Unknown option: $1"; echo ""; show_create_help; exit 1 ;;
895
+ *) user_text="$1"; shift ;;
896
+ esac
897
+ done
898
+
899
+ create_preflight
900
+
901
+ local json
902
+
903
+ if [ "$interactive" = true ]; then
904
+ json=$(create_interactive)
905
+ elif [ -n "$user_text" ]; then
906
+ json=$(create_from_text "$user_text")
907
+ else
908
+ echo -e "${BOLD}Describe what issues you want to create.${NC}"
909
+ echo -e "${DIM}Type or paste your text, then press Ctrl+D when done.${NC}"
910
+ echo ""
911
+ user_text=$(cat)
912
+ if [ -z "$user_text" ]; then
913
+ log_error "No input provided"
914
+ exit 1
915
+ fi
916
+ json=$(create_from_text "$user_text")
917
+ fi
918
+
919
+ preview_issues "$json"
920
+ confirm_and_create "$json" "$extra_label"
921
+ }
922
+
923
+ # --- Subcommand routing ---
924
+
925
+ SUBCOMMAND=""
926
+ if [[ $# -gt 0 ]] && [[ "$1" != -* ]]; then
927
+ SUBCOMMAND="$1"; shift
928
+ fi
929
+
930
+ case "$SUBCOMMAND" in
931
+ "")
932
+ while [[ $# -gt 0 ]]; do
933
+ case $1 in
934
+ --max-retries) MAX_RETRIES="$2"; shift 2 ;;
935
+ --max-turns) MAX_TURNS="$2"; shift 2 ;;
936
+ --label) ISSUE_FILTER="$2"; shift 2 ;;
937
+ --model) MODEL_FLAG="--model $2"; shift 2 ;;
938
+ -v|--version) echo "claude-queue v${VERSION}"; exit 0 ;;
939
+ -h|--help) show_help; exit 0 ;;
940
+ *) echo "Unknown option: $1"; exit 1 ;;
941
+ esac
942
+ done
943
+ main
944
+ ;;
945
+ create)
946
+ cmd_create "$@"
947
+ ;;
948
+ *)
949
+ echo "Unknown command: $SUBCOMMAND"
950
+ echo ""
951
+ show_help
952
+ exit 1
953
+ ;;
954
+ esac