-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathralph-agent.sh
More file actions
executable file
·353 lines (290 loc) · 8.33 KB
/
Copy pathralph-agent.sh
File metadata and controls
executable file
·353 lines (290 loc) · 8.33 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
#!/bin/zsh
set -e
# Ralph Agent — autonomous Claude iterations on issue PRDs
# Supports GitHub and Linear as sources
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "$HOME/dotfile/.zshrc_claude_ext"
# --- Argument parsing & interactive prompt ---
usage() {
echo "Usage: $0 [--github <issue-number>|--linear <id>] [max-iterations] [model]"
echo ""
echo "Interactive mode (no args): prompts for source and ID"
echo ""
echo "Models:"
echo " O/o - Opus (default)"
echo " S/s - Sonnet"
echo " H/h - Haiku"
echo " G/g - GLM (via Z.AI)"
echo ""
echo "Fallback chains on rate limit:"
echo " Opus → Sonnet → GLM"
echo " Sonnet → GLM"
echo " Haiku → Sonnet → GLM"
echo " GLM → Sonnet"
exit 1
}
SOURCE=""
MAX_ITERATIONS=20
MODEL_PARAM="O"
# Parse flags
while [[ $# -gt 0 ]]; do
case "$1" in
--github | -g)
SOURCE="github"
shift
# Next arg is the issue number (if present and not a flag)
if [[ $# -gt 0 && "$1" != --* && "$1" != -* ]]; then
ISSUE_NUMBER="$1"
shift
fi
;;
--linear | -l)
SOURCE="linear"
shift
if [[ $# -gt 0 && "$1" != --* && "$1" != -* ]]; then
LINEAR_ID="$1"
shift
fi
;;
--help | -h)
usage
;;
*)
# Positional args: max-iterations, model
if [[ "$1" =~ ^[0-9]+$ ]]; then
MAX_ITERATIONS="$1"
elif [[ "$1" =~ ^[OoSsHhGg]$ ]]; then
MODEL_PARAM="$1"
else
echo "Error: unknown argument '$1'"
usage
fi
shift
;;
esac
done
# Interactive source selection if not specified
if [ -z "$SOURCE" ]; then
echo "Source?"
echo " 1) GitHub"
echo " 2) Linear"
read -r "source_choice?> "
case "$source_choice" in
1 | g | G | github) SOURCE="github" ;;
2 | l | L | linear) SOURCE="linear" ;;
*)
echo "Invalid choice"
exit 1
;;
esac
fi
# Load source adapter
source "$SCRIPT_DIR/ralph-source-$SOURCE.sh"
# --- Model setup ---
case "$MODEL_PARAM" in
[Oo]) MODELS=("opus" "sonnet" "glm") ;;
[Ss]) MODELS=("sonnet" "glm") ;;
[Hh]) MODELS=("haiku" "sonnet" "glm") ;;
[Gg]) MODELS=("glm" "sonnet") ;;
*)
echo "Error: invalid model '$MODEL_PARAM'. Use O/o, S/s, H/h, or G/g."
usage
;;
esac
CURRENT_MODEL="${MODELS[1]}"
MODEL_INDEX=1
# --- Repo info ---
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)
if [ -z "$REPO" ]; then
echo "Error: not in a git repo or gh not configured"
exit 1
fi
REPO_NAME=$(basename "$REPO")
WORKTREE_BASE="$HOME/.ralph-worktrees"
# --- Ralph tools ---
setup_ralph_tools() {
setup_claude_tools
load_secret ZAI_API_KEY "op://Remote Agents/Z.AI API Key/credential"
}
# --- Model command wrapper ---
claude_model() {
local model="$1"
shift
case "$model" in
glm) claudeg "$@" ;;
claude-*) claude --model "$model" "$@" ;;
*) claude "$@" ;;
esac
}
# --- Worktree setup ---
setup_worktree() {
local suffix
suffix=$(source_worktree_suffix)
WORKTREE_PATH="$WORKTREE_BASE/$REPO_NAME-$suffix"
BRANCH_NAME=$(source_branch_name)
if [ -d "$WORKTREE_PATH" ]; then
echo "Worktree already exists at $WORKTREE_PATH"
cd "$WORKTREE_PATH"
return
fi
echo "Creating worktree at $WORKTREE_PATH..."
mkdir -p "$WORKTREE_BASE"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
git branch "$BRANCH_NAME"
fi
git worktree add "$WORKTREE_PATH" "$BRANCH_NAME"
cd "$WORKTREE_PATH"
{
echo "# Progress for $SOURCE_LABEL"
echo ""
echo "Started: $(date)"
echo ""
} >progress.txt
}
# --- Cleanup on error ---
cleanup_on_error() {
local msg="$1"
source_comment "Ralph encountered an error: $msg
Worktree: \`$WORKTREE_PATH\`
Branch: \`$BRANCH_NAME\`
Manual intervention required."
exit 1
}
# --- Main execution ---
main() {
setup_ralph_tools
# Initialize source (prompts for ID if needed)
source_init
# Fetch PRD
source_fetch_prd
# Write PRD to temp file
local prd_file
prd_file=$(mktemp)
echo "$PRD" >"$prd_file"
setup_worktree
# Copy PRD to worktree
cp "$prd_file" PRD.md
rm "$prd_file"
# Notify source we're starting
source_on_start
source_comment "Ralph starting on $SOURCE_LABEL
- Worktree: \`$WORKTREE_PATH\`
- Branch: \`$BRANCH_NAME\`
- Model: $CURRENT_MODEL
- Max iterations: $MAX_ITERATIONS"
# jq filters
local stream_text='select(.type == "assistant").message.content[]? | select(.type == "text").text // empty | gsub("\n"; "\r\n") | . + "\r\n\n"'
local final_result='select(.type == "result").result // empty'
for ((i = 1; i <= MAX_ITERATIONS; i++)); do
local iter_start
iter_start=$(date +%s)
echo "=== Iteration $i/$MAX_ITERATIONS [$(date '+%H:%M:%S')] ==="
local MAX_RETRIES=3
local RETRY_DELAY=5
local result=""
local rate_limit_detected=false
for ((retry = 1; retry <= MAX_RETRIES; retry++)); do
local tmpfile
tmpfile=$(mktemp)
trap "rm -f $tmpfile" EXIT
# Background timestamp printer (every 5 min)
(while true; do
sleep 300
echo "[timestamp: $(date '+%H:%M:%S')]"
done) &
local timestamp_pid=$!
trap "rm -f $tmpfile; kill $timestamp_pid 2>/dev/null" EXIT
claude_model "$CURRENT_MODEL" \
--verbose \
--print \
--output-format stream-json \
--allow-dangerously-skip-permissions \
--permission-mode bypassPermissions \
"You are running inside a ralph session (autonomous issue worker). See global CLAUDE.md for ralph-specific instructions.
@PRD.md @progress.txt
Find next incomplete task and execute it. ONLY DO ONE TASK PER ITERATION.
When ALL tasks complete: <promise>COMPLETE</promise>
On blocking error: <error>DESCRIPTION</error>" |
grep --line-buffered '^{' |
tee "$tmpfile" |
jq --unbuffered -rj "$stream_text"
kill $timestamp_pid 2>/dev/null
result=$(jq -r "$final_result" "$tmpfile")
rm -f "$tmpfile"
# Rate limit → model fallback
if echo "$result" | grep -qE "rate.limit|429|too.many.requests"; then
echo "=== Rate limit detected on $CURRENT_MODEL ==="
rate_limit_detected=true
break
fi
# Transient errors → retry
if echo "$result" | grep -qE "No messages returned|ECONNRESET|ETIMEDOUT|503|502"; then
echo "=== Transient error (attempt $retry/$MAX_RETRIES), retrying in ${RETRY_DELAY}s ==="
sleep "$RETRY_DELAY"
RETRY_DELAY=$((RETRY_DELAY * 2))
continue
fi
# Empty result → retry
if [ -z "$result" ]; then
echo "=== Empty result (attempt $retry/$MAX_RETRIES), retrying in ${RETRY_DELAY}s ==="
sleep "$RETRY_DELAY"
RETRY_DELAY=$((RETRY_DELAY * 2))
continue
fi
break
done
# Model fallback on rate limit
if [ "$rate_limit_detected" = true ]; then
MODEL_INDEX=$((MODEL_INDEX + 1))
if [ $MODEL_INDEX -le ${#MODELS[@]} ]; then
CURRENT_MODEL="${MODELS[$MODEL_INDEX]}"
echo "=== Falling back to $CURRENT_MODEL ==="
((i--))
continue
else
echo "=== All models exhausted, skipping iteration ==="
continue
fi
fi
# All retries exhausted
if [ -z "$result" ] && [ "$rate_limit_detected" = false ]; then
echo "=== All retries exhausted, skipping iteration ==="
continue
fi
# Iteration timing
local iter_end
iter_end=$(date +%s)
local iter_duration=$((iter_end - iter_start))
local iter_mins=$((iter_duration / 60))
local iter_secs=$((iter_duration % 60))
echo "=== Iteration $i done [$(date '+%H:%M:%S')] (${iter_mins}m ${iter_secs}s) ==="
# Completion
if echo "$result" | grep -q "<promise>COMPLETE</promise>"; then
echo "=== All tasks complete! ==="
source_on_complete
source_create_pr "$BRANCH_NAME"
exit 0
fi
# Error
if echo "$result" | grep -q "<error>"; then
local error_msg
error_msg=$(echo "$result" | grep -o "<error>.*</error>" | sed 's/<[^>]*>//g')
cleanup_on_error "$error_msg"
fi
# Progress update every 5 iterations
if [ $((i % 5)) -eq 0 ]; then
source_comment "Ralph progress update (iteration $i/$MAX_ITERATIONS):
\`\`\`
$(tail -20 progress.txt)
\`\`\`"
fi
done
echo "=== Reached max iterations ($MAX_ITERATIONS) ==="
source_comment "Ralph reached max iterations ($MAX_ITERATIONS) without completing.
Last progress:
\`\`\`
$(tail -20 progress.txt)
\`\`\`
Manual intervention may be needed."
}
main