-
Notifications
You must be signed in to change notification settings - Fork 88
Expand file tree
/
Copy pathcontinuous_claude.sh
More file actions
executable file
·2314 lines (1960 loc) · 84.5 KB
/
continuous_claude.sh
File metadata and controls
executable file
·2314 lines (1960 loc) · 84.5 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
VERSION="v0.21.0"
ADDITIONAL_FLAGS="--dangerously-skip-permissions --output-format stream-json --verbose"
NOTES_FILE="SHARED_TASK_NOTES.md"
AUTO_UPDATE=false
DISABLE_UPDATES=false
PROMPT_JQ_INSTALL="Please install jq for JSON parsing"
PROMPT_COMMIT_MESSAGE="Please review all uncommitted changes in the git repository (both modified and new files). Write a commit message with: (1) a short one-line summary, (2) two newlines, (3) then a detailed explanation. Do not include any footers or metadata like 'Generated with Claude Code' or 'Co-Authored-By'. Feel free to look at the last few commits to get a sense of the commit message style for consistency. First run 'git add .' to stage all changes including new untracked files, then commit using 'git commit -m \"your message\"' (don't push, just commit, no need to ask for confirmation)."
PROMPT_WORKFLOW_CONTEXT="## CONTINUOUS WORKFLOW CONTEXT
This is part of a continuous development loop where work happens incrementally across multiple iterations. You might run once, then a human developer might make changes, then you run again, and so on. This could happen daily or on any schedule.
**Important**: You don't need to complete the entire goal in one iteration. Just make meaningful progress on one thing, then leave clear notes for the next iteration (human or AI). Think of it as a relay race where you're passing the baton.
**Do NOT commit or push changes** - The automation will handle committing and pushing your changes after you finish. Just focus on making the code changes.
**Project Completion Signal**: If you determine that not just your current task but the ENTIRE project goal is fully complete (nothing more to be done on the overall goal), only include the exact phrase \"COMPLETION_SIGNAL_PLACEHOLDER\" in your response. Only use this when absolutely certain that the whole project is finished, not just your individual task. We will stop working on this project when multiple developers independently determine that the project is complete.
## PRIMARY GOAL"
PROMPT_NOTES_UPDATE_EXISTING="Update the \`$NOTES_FILE\` file with relevant context for the next iteration. Add new notes and remove outdated information to keep it current and useful."
PROMPT_NOTES_CREATE_NEW="Create a \`$NOTES_FILE\` file with relevant context and instructions for the next iteration."
PROMPT_NOTES_GUIDELINES="
This file helps coordinate work across iterations (both human and AI developers). It should:
- Contain relevant context and instructions for the next iteration
- Stay concise and actionable (like a notes file, not a detailed report)
- Help the next developer understand what to do next
The file should NOT include:
- Lists of completed work or full reports
- Information that can be discovered by running tests/coverage
- Unnecessary details"
PROMPT_REVIEWER_CONTEXT="## CODE REVIEW CONTEXT
You are performing a review pass on changes just made by another developer. This is NOT a new feature implementation - you are reviewing and validating existing changes using the instructions given below by the user. Feel free to use git commands to see what changes were made if it's helpful to you.
**Do NOT commit or push changes** - The automation will handle committing and pushing your changes after you finish. Just focus on validating and fixing any issues."
PROMPT_CI_FIX_CONTEXT="## CI FAILURE FIX CONTEXT
You are analyzing and fixing a CI/CD failure for a pull request.
**Your task:**
1. Inspect the failed CI workflow using the commands below
2. Analyze the error logs to understand what went wrong
3. Make the necessary code changes to fix the issue
4. Stage and commit your changes (they will be pushed to update the PR)
**Commands to inspect CI failures:**
- \`gh run list --status failure --limit 3\` - List recent failed runs
- \`gh run view <RUN_ID> --log-failed\` - View failed job logs (shorter output)
- \`gh run view <RUN_ID> --log\` - View full logs for a specific run
**Important:**
- Focus only on fixing the CI failure, not adding new features
- Make minimal changes necessary to pass CI
- If the failure seems unfixable (e.g., flaky test, infrastructure issue), explain why in your response"
PROMPT=""
MAX_RUNS=""
MAX_COST=""
MAX_DURATION=""
ENABLE_COMMITS=true
DISABLE_BRANCHES=false
GIT_BRANCH_PREFIX="continuous-claude/"
MERGE_STRATEGY="squash"
GITHUB_OWNER=""
GITHUB_REPO=""
WORKTREE_NAME=""
WORKTREE_BASE_DIR="../continuous-claude-worktrees"
CLEANUP_WORKTREE=false
LIST_WORKTREES=false
DRY_RUN=false
COMPLETION_SIGNAL="CONTINUOUS_CLAUDE_PROJECT_COMPLETE"
COMPLETION_THRESHOLD=3
ERROR_LOG=""
error_count=0
extra_iterations=0
successful_iterations=0
total_cost=0
completion_signal_count=0
i=1
EXTRA_CLAUDE_FLAGS=()
REVIEW_PROMPT=""
start_time=""
CI_RETRY_ENABLED=true
CI_RETRY_MAX_ATTEMPTS=1
parse_duration() {
# Parse a duration string like "2h", "30m", "1h30m", "90s" to seconds
# Returns: number of seconds, or empty string on error
local duration_str="$1"
# Remove all whitespace
duration_str=$(echo "$duration_str" | tr -d '[:space:]')
if [ -z "$duration_str" ]; then
return 1
fi
local total_seconds=0
local remaining="$duration_str"
# Parse hours (e.g., "2h" or "2H")
if [[ "$remaining" =~ ([0-9]+)[hH] ]]; then
local hours="${BASH_REMATCH[1]}"
total_seconds=$((total_seconds + hours * 3600))
remaining="${remaining/${BASH_REMATCH[0]}/}"
fi
# Parse minutes (e.g., "30m" or "30M")
if [[ "$remaining" =~ ([0-9]+)[mM] ]]; then
local minutes="${BASH_REMATCH[1]}"
total_seconds=$((total_seconds + minutes * 60))
remaining="${remaining/${BASH_REMATCH[0]}/}"
fi
# Parse seconds (e.g., "45s" or "45S")
if [[ "$remaining" =~ ([0-9]+)[sS] ]]; then
local seconds="${BASH_REMATCH[1]}"
total_seconds=$((total_seconds + seconds))
remaining="${remaining/${BASH_REMATCH[0]}/}"
fi
# Check if anything unparsed remains (invalid format)
if [ -n "$remaining" ]; then
return 1
fi
# Must have parsed at least something
if [ $total_seconds -eq 0 ]; then
return 1
fi
echo "$total_seconds"
return 0
}
format_duration() {
# Format seconds into a human-readable duration string
local seconds="$1"
if [ -z "$seconds" ] || [ "$seconds" -eq 0 ]; then
echo "0s"
return
fi
local hours=$((seconds / 3600))
local minutes=$(((seconds % 3600) / 60))
local secs=$((seconds % 60))
local result=""
if [ $hours -gt 0 ]; then
result="${hours}h"
fi
if [ $minutes -gt 0 ]; then
result="${result}${minutes}m"
fi
if [ $secs -gt 0 ] || [ -z "$result" ]; then
result="${result}${secs}s"
fi
echo "$result"
}
show_help() {
cat << EOF
Continuous Claude - Run Claude Code iteratively with automatic PR management
USAGE:
continuous-claude -p "prompt" (-m max-runs | --max-cost max-cost | --max-duration duration) [--owner owner] [--repo repo] [options]
continuous-claude update
REQUIRED OPTIONS:
-p, --prompt <text> The prompt/goal for Claude Code to work on
-m, --max-runs <number> Maximum number of successful iterations (use 0 for unlimited with --max-cost or --max-duration)
--max-cost <dollars> Maximum cost in USD to spend (alternative to --max-runs)
--max-duration <duration> Maximum duration to run (e.g., "2h", "30m", "1h30m") (alternative to --max-runs)
OPTIONAL FLAGS:
-h, --help Show this help message
-v, --version Show version information
--owner <owner> GitHub repository owner (auto-detected from git remote if not provided)
--repo <repo> GitHub repository name (auto-detected from git remote if not provided)
--disable-commits Disable automatic commits and PR creation
--disable-branches Commit on current branch without creating branches or PRs
--auto-update Automatically install updates when available
--disable-updates Skip all update checks and prompts
--git-branch-prefix <prefix> Branch prefix for iterations (default: "continuous-claude/")
--merge-strategy <strategy> PR merge strategy: squash, merge, or rebase (default: "squash")
--notes-file <file> Shared notes file for iteration context (default: "SHARED_TASK_NOTES.md")
--worktree <name> Run in a git worktree for parallel execution (creates if needed)
--worktree-base-dir <path> Base directory for worktrees (default: "../continuous-claude-worktrees")
--cleanup-worktree Remove worktree after completion
--cleanup-worktree Remove worktree after completion
--list-worktrees List all active git worktrees and exit
--dry-run Simulate execution without making changes
--completion-signal <phrase> Phrase that agents output when project is complete (default: "CONTINUOUS_CLAUDE_PROJECT_COMPLETE")
--completion-threshold <num> Number of consecutive signals to stop early (default: 3)
-r, --review-prompt <text> Run a reviewer pass after each iteration to validate changes
(e.g., run build/lint/tests and fix any issues)
--disable-ci-retry Disable automatic CI failure retry (enabled by default)
--ci-retry-max <number> Maximum CI fix attempts per PR (default: 1)
COMMANDS:
update Check for and install the latest version
EXAMPLES:
# Run 5 iterations to fix bugs
continuous-claude -p "Fix all linter errors" -m 5 --owner myuser --repo myproject
# Run with cost limit
continuous-claude -p "Add tests" --max-cost 10.00 --owner myuser --repo myproject
# Run for a maximum duration (time-boxed)
continuous-claude -p "Add documentation" --max-duration 2h --owner myuser --repo myproject
# Run for 30 minutes
continuous-claude -p "Refactor module" --max-duration 30m --owner myuser --repo myproject
# Run without commits (testing mode)
continuous-claude -p "Refactor code" -m 3 --disable-commits
# Run with commits on current branch (no branches or PRs)
continuous-claude -p "Quick fixes" -m 3 --disable-branches
# Use custom branch prefix and merge strategy
continuous-claude -p "Feature work" -m 10 --owner myuser --repo myproject \\
--git-branch-prefix "ai/" --merge-strategy merge
# Combine duration and cost limits (whichever comes first)
continuous-claude -p "Add tests" --max-duration 1h30m --max-cost 5.00 --owner myuser --repo myproject
# Run in a worktree for parallel execution
continuous-claude -p "Add unit tests" -m 5 --owner myuser --repo myproject --worktree instance-1
# Run multiple instances in parallel (in different terminals)
continuous-claude -p "Task A" -m 5 --owner myuser --repo myproject --worktree task-a
continuous-claude -p "Task B" -m 5 --owner myuser --repo myproject --worktree task-b
# List all active worktrees
continuous-claude --list-worktrees
# Clean up worktree after completion
continuous-claude -p "Quick fix" -m 1 --owner myuser --repo myproject \\
--worktree temp --cleanup-worktree
# Use completion signal to stop early when project is done
continuous-claude -p "Add unit tests to all files" -m 50 --owner myuser --repo myproject \\
--completion-threshold 3
# Use a reviewer to validate and fix changes after each iteration
continuous-claude -p "Add new feature" -m 5 --owner myuser --repo myproject \\
-r "Run npm test and npm run lint, fix any failures"
# Allow up to 2 CI fix attempts per PR (default is 1)
continuous-claude -p "Add tests" -m 5 --owner myuser --repo myproject --ci-retry-max 2
# Disable automatic CI failure retry
continuous-claude -p "Add tests" -m 5 --owner myuser --repo myproject --disable-ci-retry
# Check for and install updates
continuous-claude update
REQUIREMENTS:
- Claude Code CLI (https://claude.ai/code)
- GitHub CLI (gh) - authenticated with 'gh auth login'
- jq - JSON parsing utility
- Git repository (unless --disable-commits is used)
NOTE:
continuous-claude automatically checks for updates at startup. You can press 'N' to skip the update.
For more information, visit: https://github.com/AnandChowdhary/continuous-claude
EOF
}
show_version() {
echo "continuous-claude version $VERSION"
}
get_latest_version() {
# Fetch the latest release version from GitHub using gh CLI
local latest_version
if ! command -v gh &> /dev/null; then
return 1
fi
latest_version=$(gh release view --repo AnandChowdhary/continuous-claude --json tagName --jq '.tagName' 2>/dev/null)
if [ -z "$latest_version" ]; then
return 1
fi
echo "$latest_version"
return 0
}
convert_gitmoji() {
# Convert gitmoji codes to actual emoji characters
sed -e 's/:sparkles:/✨/g' \
-e 's/:bug:/🐛/g' \
-e 's/:bookmark:/🔖/g' \
-e 's/:recycle:/♻️/g' \
-e 's/:art:/🎨/g' \
-e 's/:pencil:/✏️/g' \
-e 's/:memo:/📝/g' \
-e 's/:construction_worker:/👷/g' \
-e 's/:rocket:/🚀/g' \
-e 's/:white_check_mark:/✅/g' \
-e 's/:lock:/🔒/g' \
-e 's/:fire:/🔥/g' \
-e 's/:ambulance:/🚑/g' \
-e 's/:lipstick:/💄/g' \
-e 's/:rotating_light:/🚨/g' \
-e 's/:construction:/🚧/g' \
-e 's/:green_heart:/💚/g' \
-e 's/:arrow_down:/⬇️/g' \
-e 's/:arrow_up:/⬆️/g' \
-e 's/:pushpin:/📌/g' \
-e 's/:tada:/🎉/g' \
-e 's/:wrench:/🔧/g' \
-e 's/:hammer:/🔨/g' \
-e 's/:package:/📦/g' \
-e 's/:truck:/🚚/g' \
-e 's/:bento:/🍱/g' \
-e 's/:wheelchair:/♿/g' \
-e 's/:bulb:/💡/g' \
-e 's/:beers:/🍻/g' \
-e 's/:speech_balloon:/💬/g' \
-e 's/:card_file_box:/🗃️/g' \
-e 's/:loud_sound:/🔊/g' \
-e 's/:mute:/🔇/g' \
-e 's/:busts_in_silhouette:/👥/g' \
-e 's/:children_crossing:/🚸/g' \
-e 's/:building_construction:/🏗️/g' \
-e 's/:iphone:/📱/g' \
-e 's/:clown_face:/🤡/g' \
-e 's/:egg:/🥚/g' \
-e 's/:see_no_evil:/🙈/g' \
-e 's/:camera_flash:/📸/g' \
-e 's/:alembic:/⚗️/g' \
-e 's/:mag:/🔍/g' \
-e 's/:label:/🏷️/g' \
-e 's/:seedling:/🌱/g' \
-e 's/:triangular_flag_on_post:/🚩/g' \
-e 's/:goal_net:/🥅/g' \
-e 's/:dizzy:/💫/g' \
-e 's/:wastebasket:/🗑️/g' \
-e 's/:passport_control:/🛂/g' \
-e 's/:adhesive_bandage:/🩹/g' \
-e 's/:monocle_face:/🧐/g' \
-e 's/:coffin:/⚰️/g' \
-e 's/:test_tube:/🧪/g' \
-e 's/:necktie:/👔/g' \
-e 's/:stethoscope:/🩺/g' \
-e 's/:bricks:/🧱/g' \
-e 's/:technologist:/🧑💻/g' \
-e 's/:zap:/⚡/g' \
-e 's/:heavy_plus_sign:/➕/g' \
-e 's/:heavy_minus_sign:/➖/g' \
-e 's/:twisted_rightwards_arrows:/🔀/g' \
-e 's/:rewind:/⏪/g' \
-e 's/:boom:/💥/g' \
-e 's/:ok_hand:/👌/g' \
-e 's/:new:/🆕/g' \
-e 's/:up:/🆙/g'
}
get_release_notes() {
# Fetch release notes for a specific version from GitHub
local version="$1"
if ! command -v gh &> /dev/null; then
return 1
fi
local notes
notes=$(gh release view "$version" --repo AnandChowdhary/continuous-claude --json body --jq '.body' 2>/dev/null)
if [ -z "$notes" ]; then
return 1
fi
echo "$notes" | convert_gitmoji
return 0
}
compare_versions() {
# Compare two version strings (e.g., "v0.9.1" and "v0.10.0")
# Returns 0 if they're equal, 1 if first is older, 2 if first is newer
local ver1="$1"
local ver2="$2"
# Remove 'v' prefix if present
ver1="${ver1#v}"
ver2="${ver2#v}"
# Remove any pre-release suffix (e.g., -beta, -rc1) for simple comparison
ver1="${ver1%%-*}"
ver2="${ver2%%-*}"
if [ "$ver1" = "$ver2" ]; then
return 0
fi
# Split versions and compare using safer array creation
local IFS=.
local i ver1_arr ver2_arr
read -ra ver1_arr <<< "$ver1"
read -ra ver2_arr <<< "$ver2"
# Fill empty positions with zeros
for ((i=${#ver1_arr[@]}; i<${#ver2_arr[@]}; i++)); do
ver1_arr[i]=0
done
for ((i=${#ver2_arr[@]}; i<${#ver1_arr[@]}; i++)); do
ver2_arr[i]=0
done
# Compare each component, fallback to string comparison if non-numeric
for ((i=0; i<${#ver1_arr[@]}; i++)); do
local c1="${ver1_arr[i]}"
local c2="${ver2_arr[i]}"
if [[ "$c1" =~ ^[0-9]+$ ]] && [[ "$c2" =~ ^[0-9]+$ ]]; then
if ((10#$c1 < 10#$c2)); then
return 1
fi
if ((10#$c1 > 10#$c2)); then
return 2
fi
else
# Fallback: string comparison for non-numeric components
if [[ "$c1" < "$c2" ]]; then
return 1
fi
if [[ "$c1" > "$c2" ]]; then
return 2
fi
fi
done
return 0
}
get_script_path() {
# Get the absolute path to the current script
local script_path
script_path=$(readlink -f "$0" 2>/dev/null || realpath "$0" 2>/dev/null || echo "$0")
echo "$script_path"
}
download_and_install_update() {
local latest_version="$1"
local script_path="$2"
echo "📥 Downloading version $latest_version..." >&2
# Download the new version to a temporary file
local temp_file=$(mktemp)
# Use the specific release tag instead of main branch
local download_url="https://raw.githubusercontent.com/AnandChowdhary/continuous-claude/${latest_version}/continuous_claude.sh"
local checksum_url="https://raw.githubusercontent.com/AnandChowdhary/continuous-claude/${latest_version}/continuous_claude.sh.sha256"
if ! curl -fsSL "$download_url" -o "$temp_file"; then
echo "❌ Failed to download update" >&2
rm -f "$temp_file"
return 1
fi
# Download the checksum file
local checksum_file=$(mktemp)
if ! curl -fsSL "$checksum_url" -o "$checksum_file"; then
echo "❌ Failed to download checksum file" >&2
rm -f "$temp_file" "$checksum_file"
return 1
fi
# Verify checksum
local expected_checksum
expected_checksum=$(cat "$checksum_file" | awk '{print $1}')
local actual_checksum
actual_checksum=$(sha256sum "$temp_file" | awk '{print $1}')
if [ "$expected_checksum" != "$actual_checksum" ]; then
echo "❌ Checksum verification failed! Update aborted." >&2
rm -f "$temp_file" "$checksum_file"
return 1
fi
rm -f "$checksum_file"
# Verify the downloaded file is valid bash
if ! bash -n "$temp_file" 2>/dev/null; then
echo "❌ Downloaded file has invalid syntax" >&2
rm -f "$temp_file"
return 1
fi
# Make it executable
chmod +x "$temp_file"
# Replace the current script
if ! mv "$temp_file" "$script_path"; then
echo "❌ Failed to replace script (permission denied?)" >&2
rm -f "$temp_file"
return 1
fi
echo "✅ Updated to version $latest_version" >&2
return 0
}
check_for_updates() {
local skip_prompt="$1"
if [ "$DISABLE_UPDATES" = "true" ]; then
return 0
fi
# Get the latest version
local latest_version
if ! latest_version=$(get_latest_version); then
# Silently fail if we can't check for updates
return 0
fi
# Compare versions
compare_versions "$VERSION" "$latest_version"
local comparison=$?
if [ $comparison -eq 1 ]; then
# Current version is older
echo "" >&2
echo "🆕 A new version of continuous-claude is available: $latest_version (current: $VERSION)" >&2
# Display release notes if available
local release_notes
if release_notes=$(get_release_notes "$latest_version"); then
echo "" >&2
echo "📋 Release notes:" >&2
echo "─────────────────────────────────────────" >&2
echo "$release_notes" >&2
echo "─────────────────────────────────────────" >&2
fi
if [ "$skip_prompt" = "true" ]; then
return 0
fi
echo "" >&2
local response
if [ "$AUTO_UPDATE" = "true" ]; then
response="y"
else
echo -n "Would you like to update now? [y/N] " >&2
if ! read -t 60 -r response; then
echo "" >&2
echo "⏱️ No response received within 60 seconds, skipping update." >&2
response="n"
fi
fi
if [[ "$response" =~ ^[Yy]$ ]]; then
local script_path=$(get_script_path)
if download_and_install_update "$latest_version" "$script_path"; then
echo "🔄 Restarting with new version..." >&2
# Restart the script with the original arguments
# This happens early in startup before main application logic runs
exec "$script_path" "$@"
else
echo "⚠️ Update failed. Continuing with current version." >&2
fi
else
echo "⏭️ Skipping update. You can update later with: continuous-claude update" >&2
fi
fi
return 0
}
handle_update_command() {
if [ "$DISABLE_UPDATES" = "true" ]; then
echo "⚠️ Updates are disabled via --disable-updates flag. Skipping." >&2
exit 0
fi
echo "🔍 Checking for updates..." >&2
local latest_version
if ! latest_version=$(get_latest_version); then
echo "❌ Failed to check for updates. Make sure 'gh' CLI is installed and authenticated." >&2
exit 1
fi
compare_versions "$VERSION" "$latest_version"
local comparison=$?
if [ $comparison -eq 0 ]; then
echo "✅ You're already on the latest version ($VERSION)" >&2
exit 0
elif [ $comparison -eq 2 ]; then
echo "ℹ️ You're on a newer version ($VERSION) than the latest release ($latest_version)" >&2
exit 0
fi
# Current version is older
echo "🆕 New version available: $latest_version (current: $VERSION)" >&2
# Display release notes if available
local release_notes
if release_notes=$(get_release_notes "$latest_version"); then
echo "" >&2
echo "📋 Release notes:" >&2
echo "─────────────────────────────────────────" >&2
echo "$release_notes" >&2
echo "─────────────────────────────────────────" >&2
fi
echo "" >&2
local response
if [ "$AUTO_UPDATE" = "true" ]; then
response="y"
else
echo -n "Would you like to update now? [y/N] " >&2
if ! read -t 60 -r response; then
echo "" >&2
echo "⏱️ No response received within 60 seconds, skipping update." >&2
response="n"
fi
fi
if [[ "$response" =~ ^[Yy]$ ]]; then
local script_path=$(get_script_path)
if download_and_install_update "$latest_version" "$script_path"; then
echo "✅ Update complete! Version $latest_version is now installed." >&2
exit 0
else
echo "❌ Update failed." >&2
exit 1
fi
else
echo "⏭️ Update cancelled." >&2
exit 0
fi
}
detect_github_repo() {
# Try to detect GitHub owner and repo from git remote
# Returns: "owner repo" on success, empty string on failure
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
return 1
fi
# Try to get the origin remote URL
local remote_url
if ! remote_url=$(git remote get-url origin 2>/dev/null); then
return 1
fi
# Parse GitHub URL (supports both HTTPS and SSH formats)
# HTTPS: https://github.com/owner/repo.git or https://github.com/owner/repo
# SSH: git@github.com:owner/repo.git or git@github.com:owner/repo
local owner=""
local repo=""
if [[ "$remote_url" =~ ^https://github\.com/([^/]+)/([^/]+)$ ]]; then
# HTTPS format
owner="${BASH_REMATCH[1]}"
repo="${BASH_REMATCH[2]}"
elif [[ "$remote_url" =~ ^git@github\.com:([^/]+)/([^/]+)$ ]]; then
# SSH format
owner="${BASH_REMATCH[1]}"
repo="${BASH_REMATCH[2]}"
else
return 1
fi
# Remove .git suffix if present
repo="${repo%.git}"
# Validate that we got both owner and repo
if [ -z "$owner" ] || [ -z "$repo" ]; then
return 1
fi
echo "$owner $repo"
return 0
}
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_help
exit 0
;;
-v|--version)
show_version
exit 0
;;
-p|--prompt)
PROMPT="$2"
shift 2
;;
-m|--max-runs)
MAX_RUNS="$2"
shift 2
;;
--max-cost)
MAX_COST="$2"
shift 2
;;
--max-duration)
MAX_DURATION="$2"
shift 2
;;
--git-branch-prefix)
GIT_BRANCH_PREFIX="$2"
shift 2
;;
--merge-strategy)
MERGE_STRATEGY="$2"
shift 2
;;
--owner)
GITHUB_OWNER="$2"
shift 2
;;
--repo)
GITHUB_REPO="$2"
shift 2
;;
--disable-commits)
ENABLE_COMMITS=false
shift
;;
--disable-branches)
DISABLE_BRANCHES=true
shift
;;
--auto-update)
AUTO_UPDATE=true
shift
;;
--disable-updates)
DISABLE_UPDATES=true
shift
;;
--notes-file)
NOTES_FILE="$2"
shift 2
;;
--worktree)
WORKTREE_NAME="$2"
shift 2
;;
--worktree-base-dir)
WORKTREE_BASE_DIR="$2"
shift 2
;;
--cleanup-worktree)
CLEANUP_WORKTREE=true
shift
;;
--list-worktrees)
LIST_WORKTREES=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--completion-signal)
COMPLETION_SIGNAL="$2"
shift 2
;;
--completion-threshold)
COMPLETION_THRESHOLD="$2"
shift 2
;;
-r|--review-prompt)
REVIEW_PROMPT="$2"
shift 2
;;
--disable-ci-retry)
CI_RETRY_ENABLED=false
shift
;;
--ci-retry-max)
CI_RETRY_MAX_ATTEMPTS="$2"
shift 2
;;
*)
# Collect unknown flags to forward to claude
EXTRA_CLAUDE_FLAGS+=("$1")
shift
;;
esac
done
}
parse_update_flags() {
while [[ $# -gt 0 ]]; do
case $1 in
--auto-update)
AUTO_UPDATE=true
shift
;;
--disable-updates)
DISABLE_UPDATES=true
shift
;;
-h|--help)
show_help
exit 0
;;
*)
echo "❌ Unknown flag for update command: $1" >&2
exit 1
;;
esac
done
}
validate_arguments() {
if [ -z "$PROMPT" ]; then
echo "❌ Error: Prompt is required. Use -p to provide a prompt." >&2
echo "Run '$0 --help' for usage information." >&2
exit 1
fi
if [ -z "$MAX_RUNS" ] && [ -z "$MAX_COST" ] && [ -z "$MAX_DURATION" ]; then
echo "❌ Error: Either --max-runs, --max-cost, or --max-duration is required." >&2
echo "Run '$0 --help' for usage information." >&2
exit 1
fi
if [ -n "$MAX_RUNS" ] && ! [[ "$MAX_RUNS" =~ ^[0-9]+$ ]]; then
echo "❌ Error: --max-runs must be a non-negative integer" >&2
exit 1
fi
if [ -n "$MAX_COST" ]; then
if ! [[ "$MAX_COST" =~ ^[0-9]+\.?[0-9]*$ ]] || [ "$(awk "BEGIN {print ($MAX_COST <= 0)}")" = "1" ]; then
echo "❌ Error: --max-cost must be a positive number" >&2
exit 1
fi
fi
if [ -n "$MAX_DURATION" ]; then
local duration_seconds
if ! duration_seconds=$(parse_duration "$MAX_DURATION"); then
echo "❌ Error: --max-duration must be a valid duration (e.g., '2h', '30m', '1h30m', '90s')" >&2
exit 1
fi
# Store parsed duration in seconds back to MAX_DURATION for later use
MAX_DURATION="$duration_seconds"
fi
if [[ ! "$MERGE_STRATEGY" =~ ^(squash|merge|rebase)$ ]]; then
echo "❌ Error: --merge-strategy must be one of: squash, merge, rebase" >&2
exit 1
fi
if [ -n "$COMPLETION_THRESHOLD" ]; then
if ! [[ "$COMPLETION_THRESHOLD" =~ ^[0-9]+$ ]] || [ "$COMPLETION_THRESHOLD" -lt 1 ]; then
echo "❌ Error: --completion-threshold must be a positive integer" >&2
exit 1
fi
fi
if [ -n "$CI_RETRY_MAX_ATTEMPTS" ]; then
if ! [[ "$CI_RETRY_MAX_ATTEMPTS" =~ ^[0-9]+$ ]] || [ "$CI_RETRY_MAX_ATTEMPTS" -lt 1 ]; then
echo "❌ Error: --ci-retry-max must be a positive integer" >&2
exit 1
fi
fi
# Only require GitHub info if commits are enabled
if [ "$ENABLE_COMMITS" = "true" ]; then
# Auto-detect owner and repo if not provided
if [ -z "$GITHUB_OWNER" ] || [ -z "$GITHUB_REPO" ]; then
local detected_info
if detected_info=$(detect_github_repo); then
# Parse the detected owner and repo
local detected_owner=$(echo "$detected_info" | awk '{print $1}')
local detected_repo=$(echo "$detected_info" | awk '{print $2}')
# Only use detected values if not already provided
if [ -z "$GITHUB_OWNER" ]; then
GITHUB_OWNER="$detected_owner"
fi
if [ -z "$GITHUB_REPO" ]; then
GITHUB_REPO="$detected_repo"
fi
fi
fi
# After detection attempt, verify both are set
if [ -z "$GITHUB_OWNER" ]; then
echo "❌ Error: GitHub owner is required. Use --owner to provide the owner, or run from a git repository with a GitHub remote." >&2
echo "Run '$0 --help' for usage information." >&2
exit 1
fi
if [ -z "$GITHUB_REPO" ]; then
echo "❌ Error: GitHub repo is required. Use --repo to provide the repo, or run from a git repository with a GitHub remote." >&2
echo "Run '$0 --help' for usage information." >&2
exit 1
fi
fi
}
validate_requirements() {
if ! command -v claude &> /dev/null; then
echo "❌ Error: Claude Code is not installed: https://claude.ai/code" >&2
exit 1
fi
if ! command -v jq &> /dev/null; then
echo "⚠️ jq is required for JSON parsing but is not installed. Asking Claude Code to install it..." >&2
claude -p "$PROMPT_JQ_INSTALL" --allowedTools "Bash,Read"
if ! command -v jq &> /dev/null; then
echo "❌ Error: jq is still not installed after Claude Code attempt." >&2
exit 1
fi
fi
# Only check for GitHub CLI if commits are enabled
if [ "$ENABLE_COMMITS" = "true" ]; then
if ! command -v gh &> /dev/null; then
echo "❌ Error: GitHub CLI (gh) is not installed: https://cli.github.com" >&2
exit 1
fi
if ! gh auth status >/dev/null 2>&1; then
echo "❌ Error: GitHub CLI is not authenticated. Run 'gh auth login' first." >&2
exit 1
fi
fi
}
wait_for_pr_checks() {
local pr_number="$1"
local owner="$2"
local repo="$3"
local iteration_display="$4"
local max_iterations=180 # 180 * 10 seconds = 30 minutes
local iteration=0
local prev_check_count=""
local prev_success_count=""
local prev_pending_count=""
local prev_failed_count=""
local prev_review_status=""
local prev_no_checks_configured=""
local waiting_message_printed=false
while [ $iteration -lt $max_iterations ]; do
local checks_json
local no_checks_configured=false
if ! checks_json=$(gh pr checks "$pr_number" --repo "$owner/$repo" --json state,bucket 2>&1); then
if echo "$checks_json" | grep -q "no checks"; then
no_checks_configured=true
checks_json="[]"
else
echo "⚠️ $iteration_display Failed to get PR checks status: $checks_json" >&2
return 1
fi
fi
local check_count=$(echo "$checks_json" | jq 'length' 2>/dev/null || echo "0")
local all_completed=true
local all_success=true
if [ "$no_checks_configured" = "false" ] && [ "$check_count" -eq 0 ]; then
all_completed=false
fi
local pending_count=0
local success_count=0
local failed_count=0
if [ "$check_count" -gt 0 ]; then
local idx=0
while [ $idx -lt $check_count ]; do
local state=$(echo "$checks_json" | jq -r ".[$idx].state")
local bucket=$(echo "$checks_json" | jq -r ".[$idx].bucket // \"pending\"")