wiggum-cli 0.14.0 → 0.16.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.
- package/README.md +30 -5
- package/dist/ai/providers.js +19 -14
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +2 -2
- package/dist/generator/templates.d.ts +1 -0
- package/dist/generator/templates.js +14 -1
- package/dist/index.d.ts +14 -1
- package/dist/index.js +229 -41
- package/dist/repl/session-state.d.ts +2 -0
- package/dist/templates/config/ralph.config.cjs.tmpl +1 -1
- package/dist/templates/prompts/PROMPT_e2e.md.tmpl +151 -0
- package/dist/templates/prompts/PROMPT_feature.md.tmpl +23 -0
- package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +13 -42
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +168 -0
- package/dist/templates/scripts/feature-loop-actions.test.ts +92 -0
- package/dist/templates/scripts/feature-loop.sh.tmpl +334 -40
- package/dist/tui/app.d.ts +16 -1
- package/dist/tui/app.js +31 -5
- package/dist/tui/components/ActivityFeed.d.ts +18 -0
- package/dist/tui/components/ActivityFeed.js +31 -0
- package/dist/tui/components/ChatInput.d.ts +3 -1
- package/dist/tui/components/ChatInput.js +23 -4
- package/dist/tui/components/CommandDropdown.d.ts +3 -1
- package/dist/tui/components/CommandDropdown.js +10 -7
- package/dist/tui/components/RunCompletionSummary.d.ts +27 -1
- package/dist/tui/components/RunCompletionSummary.js +97 -7
- package/dist/tui/components/SpecCompletionSummary.d.ts +3 -2
- package/dist/tui/components/SpecCompletionSummary.js +26 -9
- package/dist/tui/components/SummaryBox.d.ts +4 -3
- package/dist/tui/components/SummaryBox.js +7 -3
- package/dist/tui/hooks/useBackgroundRuns.js +1 -1
- package/dist/tui/orchestration/interview-orchestrator.js +35 -5
- package/dist/tui/screens/MainShell.js +2 -1
- package/dist/tui/screens/RunScreen.d.ts +15 -15
- package/dist/tui/screens/RunScreen.js +139 -17
- package/dist/tui/utils/action-inbox.d.ts +43 -0
- package/dist/tui/utils/action-inbox.js +109 -0
- package/dist/tui/utils/build-run-summary.js +4 -1
- package/dist/tui/utils/git-summary.d.ts +13 -0
- package/dist/tui/utils/git-summary.js +30 -0
- package/dist/tui/utils/loop-status.d.ts +54 -0
- package/dist/tui/utils/loop-status.js +213 -1
- package/dist/tui/utils/polishGoal.d.ts +37 -0
- package/dist/tui/utils/polishGoal.js +170 -0
- package/dist/utils/ci.d.ts +8 -0
- package/dist/utils/ci.js +13 -0
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/fuzzy-match.d.ts +5 -0
- package/dist/utils/fuzzy-match.js +16 -0
- package/dist/utils/spec-names.d.ts +6 -0
- package/dist/utils/spec-names.js +27 -0
- package/package.json +15 -5
- package/src/templates/config/ralph.config.cjs.tmpl +1 -1
- package/src/templates/prompts/PROMPT_e2e.md.tmpl +151 -0
- package/src/templates/prompts/PROMPT_feature.md.tmpl +23 -0
- package/src/templates/prompts/PROMPT_review_auto.md.tmpl +13 -42
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +168 -0
- package/src/templates/scripts/feature-loop-actions.test.ts +92 -0
- package/src/templates/scripts/feature-loop.sh.tmpl +334 -40
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
# Options:
|
|
7
7
|
# --worktree Use git worktree for isolation (enables parallel execution)
|
|
8
8
|
# --resume Resume an interrupted loop (reuses existing branch/worktree)
|
|
9
|
-
# --model MODEL Claude model to use (e.g., opus, sonnet, claude-sonnet-4-5-
|
|
10
|
-
# --review-mode MODE Review mode: 'manual' (stop at PR) or '
|
|
9
|
+
# --model MODEL Claude model to use (e.g., opus, sonnet, claude-sonnet-4-5-20250929)
|
|
10
|
+
# --review-mode MODE Review mode: 'manual' (stop at PR), 'auto' (review, no merge), or 'merge' (review + merge). Default: 'manual'
|
|
11
11
|
|
|
12
12
|
set -e
|
|
13
13
|
set -o pipefail
|
|
@@ -91,14 +91,14 @@ if [ -z "$REVIEW_MODE" ]; then
|
|
|
91
91
|
fi
|
|
92
92
|
|
|
93
93
|
# Validate review mode
|
|
94
|
-
if [ "$REVIEW_MODE" != "manual" ] && [ "$REVIEW_MODE" != "auto" ]; then
|
|
95
|
-
echo "ERROR: Invalid review mode: '$REVIEW_MODE'. Allowed values are 'manual' or '
|
|
94
|
+
if [ "$REVIEW_MODE" != "manual" ] && [ "$REVIEW_MODE" != "auto" ] && [ "$REVIEW_MODE" != "merge" ]; then
|
|
95
|
+
echo "ERROR: Invalid review mode: '$REVIEW_MODE'. Allowed values are 'manual', 'auto', or 'merge'." >&2
|
|
96
96
|
exit 1
|
|
97
97
|
fi
|
|
98
98
|
|
|
99
99
|
# Build claude commands
|
|
100
|
-
CLAUDE_CMD_OPUS="claude -p --dangerously-skip-permissions --model ${PLANNING_MODEL}"
|
|
101
|
-
CLAUDE_CMD_IMPL="claude -p --dangerously-skip-permissions --model ${MODEL:-$DEFAULT_MODEL}"
|
|
100
|
+
CLAUDE_CMD_OPUS="claude -p --output-format json --dangerously-skip-permissions --model ${PLANNING_MODEL}"
|
|
101
|
+
CLAUDE_CMD_IMPL="claude -p --output-format json --dangerously-skip-permissions --model ${MODEL:-$DEFAULT_MODEL}"
|
|
102
102
|
|
|
103
103
|
# Token tracking
|
|
104
104
|
TOKENS_FILE="/tmp/ralph-loop-${1}.tokens"
|
|
@@ -107,35 +107,192 @@ STATUS_FILE="/tmp/ralph-loop-${1}.status"
|
|
|
107
107
|
FINAL_STATUS_FILE="/tmp/ralph-loop-${1}.final"
|
|
108
108
|
PHASES_FILE="/tmp/ralph-loop-${1}.phases"
|
|
109
109
|
BASELINE_FILE="/tmp/ralph-loop-${1}.baseline"
|
|
110
|
+
SESSIONS_FILE="/tmp/ralph-loop-${1}.sessions"
|
|
111
|
+
LOG_FILE="/tmp/ralph-loop-${1}.log"
|
|
110
112
|
|
|
111
|
-
# Initialize token tracking
|
|
113
|
+
# Initialize token tracking (4-field format: input|output|cache_create|cache_read)
|
|
112
114
|
init_tokens() {
|
|
113
|
-
echo "0|0" > "$TOKENS_FILE"
|
|
115
|
+
echo "0|0|0|0" > "$TOKENS_FILE"
|
|
116
|
+
> "$SESSIONS_FILE"
|
|
117
|
+
> "$LOG_FILE"
|
|
114
118
|
}
|
|
115
119
|
|
|
116
|
-
#
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
# Extract session result from JSON output.
|
|
121
|
+
# Writes human-readable result text to the .log file and captures session_id.
|
|
122
|
+
# Usage: extract_session_result <raw_json_file>
|
|
123
|
+
# Sets: LAST_SESSION_ID variable
|
|
124
|
+
extract_session_result() {
|
|
125
|
+
local raw_file="$1"
|
|
126
|
+
LAST_SESSION_ID=""
|
|
127
|
+
if [ ! -f "$raw_file" ]; then return; fi
|
|
128
|
+
|
|
129
|
+
local result
|
|
130
|
+
result=$(python3 -c "
|
|
131
|
+
import json, sys
|
|
132
|
+
try:
|
|
133
|
+
data = json.load(open(sys.argv[1]))
|
|
134
|
+
if not isinstance(data, list): data = [data]
|
|
135
|
+
for entry in reversed(data):
|
|
136
|
+
if isinstance(entry, dict) and entry.get('type') == 'result':
|
|
137
|
+
print(entry.get('session_id', ''))
|
|
138
|
+
break
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
" "$raw_file" 2>/dev/null) || true
|
|
121
142
|
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
LAST_SESSION_ID="$result"
|
|
144
|
+
|
|
145
|
+
if [ -n "$LAST_SESSION_ID" ]; then
|
|
146
|
+
echo "$LAST_SESSION_ID" >> "$SESSIONS_FILE"
|
|
147
|
+
fi
|
|
124
148
|
|
|
149
|
+
# Extract human-readable result text and append to log
|
|
150
|
+
python3 -c "
|
|
151
|
+
import json, sys
|
|
152
|
+
try:
|
|
153
|
+
data = json.load(open(sys.argv[1]))
|
|
154
|
+
if not isinstance(data, list): data = [data]
|
|
155
|
+
for entry in data:
|
|
156
|
+
if isinstance(entry, dict) and entry.get('type') == 'result':
|
|
157
|
+
text = entry.get('result', '')
|
|
158
|
+
if text:
|
|
159
|
+
print(text)
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
" "$raw_file" >> "$LOG_FILE" 2>/dev/null || true
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Accumulate tokens from a session JSONL file into the .tokens file.
|
|
166
|
+
# Usage: accumulate_tokens_from_session <session_id>
|
|
167
|
+
accumulate_tokens_from_session() {
|
|
168
|
+
local session_id="$1"
|
|
169
|
+
if [ -z "$session_id" ]; then return; fi
|
|
170
|
+
|
|
171
|
+
# Find the JSONL file for this session
|
|
172
|
+
local jsonl_file=""
|
|
173
|
+
for f in ~/.claude/projects/*/"${session_id}.jsonl"; do
|
|
174
|
+
if [ -f "$f" ]; then
|
|
175
|
+
jsonl_file="$f"
|
|
176
|
+
break
|
|
177
|
+
fi
|
|
178
|
+
done
|
|
179
|
+
|
|
180
|
+
if [ -z "$jsonl_file" ]; then
|
|
181
|
+
echo "WARNING: Could not find JSONL for session $session_id" >&2
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# Extract and sum token usage from all assistant messages
|
|
186
|
+
local session_tokens
|
|
187
|
+
session_tokens=$(python3 -c "
|
|
188
|
+
import json, sys
|
|
189
|
+
totals = {'input': 0, 'output': 0, 'cache_create': 0, 'cache_read': 0}
|
|
190
|
+
for line in open(sys.argv[1]):
|
|
191
|
+
try:
|
|
192
|
+
obj = json.loads(line)
|
|
193
|
+
if obj.get('type') != 'assistant':
|
|
194
|
+
continue
|
|
195
|
+
usage = obj.get('message', {}).get('usage', {})
|
|
196
|
+
if not usage:
|
|
197
|
+
continue
|
|
198
|
+
totals['input'] += usage.get('input_tokens', 0)
|
|
199
|
+
totals['output'] += usage.get('output_tokens', 0)
|
|
200
|
+
totals['cache_create'] += usage.get('cache_creation_input_tokens', 0)
|
|
201
|
+
totals['cache_read'] += usage.get('cache_read_input_tokens', 0)
|
|
202
|
+
except (json.JSONDecodeError, AttributeError):
|
|
203
|
+
continue
|
|
204
|
+
print(f\"{totals['input']}|{totals['output']}|{totals['cache_create']}|{totals['cache_read']}\")
|
|
205
|
+
" "$jsonl_file" 2>/dev/null) || true
|
|
206
|
+
|
|
207
|
+
if [ -z "$session_tokens" ]; then return; fi
|
|
208
|
+
|
|
209
|
+
# Parse session tokens
|
|
210
|
+
local s_input s_output s_cache_create s_cache_read
|
|
211
|
+
s_input=$(echo "$session_tokens" | cut -d'|' -f1)
|
|
212
|
+
s_output=$(echo "$session_tokens" | cut -d'|' -f2)
|
|
213
|
+
s_cache_create=$(echo "$session_tokens" | cut -d'|' -f3)
|
|
214
|
+
s_cache_read=$(echo "$session_tokens" | cut -d'|' -f4)
|
|
215
|
+
|
|
216
|
+
# Read current totals
|
|
217
|
+
local current c_input c_output c_cache_create c_cache_read
|
|
125
218
|
if [ -f "$TOKENS_FILE" ]; then
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
219
|
+
current=$(cat "$TOKENS_FILE")
|
|
220
|
+
c_input=$(echo "$current" | cut -d'|' -f1)
|
|
221
|
+
c_output=$(echo "$current" | cut -d'|' -f2)
|
|
222
|
+
c_cache_create=$(echo "$current" | cut -d'|' -f3)
|
|
223
|
+
c_cache_read=$(echo "$current" | cut -d'|' -f4)
|
|
224
|
+
[[ "$c_input" =~ ^[0-9]+$ ]] || c_input=0
|
|
225
|
+
[[ "$c_output" =~ ^[0-9]+$ ]] || c_output=0
|
|
226
|
+
[[ "$c_cache_create" =~ ^[0-9]+$ ]] || c_cache_create=0
|
|
227
|
+
[[ "$c_cache_read" =~ ^[0-9]+$ ]] || c_cache_read=0
|
|
131
228
|
else
|
|
132
|
-
|
|
133
|
-
|
|
229
|
+
c_input=0; c_output=0; c_cache_create=0; c_cache_read=0
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
# Accumulate
|
|
233
|
+
echo "$((c_input + s_input))|$((c_output + s_output))|$((c_cache_create + s_cache_create))|$((c_cache_read + s_cache_read))" > "$TOKENS_FILE"
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Action inbox: write request file if not already present
|
|
237
|
+
write_action_request() {
|
|
238
|
+
local action_file="/tmp/ralph-loop-${FEATURE}.action.json"
|
|
239
|
+
if [ -f "$action_file" ]; then
|
|
240
|
+
echo "WARNING: Action request file already exists, skipping write: $action_file" >&2
|
|
241
|
+
return 0
|
|
134
242
|
fi
|
|
243
|
+
cat > "$action_file" << 'EOF'
|
|
244
|
+
{
|
|
245
|
+
"id": "post_pr_choice",
|
|
246
|
+
"prompt": "Loop complete. What would you like to do?",
|
|
247
|
+
"choices": [
|
|
248
|
+
{"id": "done", "label": "Done — end loop"},
|
|
249
|
+
{"id": "merge_local", "label": "Merge back to main locally"},
|
|
250
|
+
{"id": "keep_branch", "label": "Keep branch as-is"},
|
|
251
|
+
{"id": "discard", "label": "Discard this work"}
|
|
252
|
+
],
|
|
253
|
+
"default": "done"
|
|
254
|
+
}
|
|
255
|
+
EOF
|
|
256
|
+
echo "Action request written: $action_file"
|
|
257
|
+
}
|
|
135
258
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
259
|
+
# Action inbox: poll for reply file, fallback to default after 15 minutes
|
|
260
|
+
poll_action_reply() {
|
|
261
|
+
local action_file="/tmp/ralph-loop-${FEATURE}.action.json"
|
|
262
|
+
local reply_file="/tmp/ralph-loop-${FEATURE}.action.reply.json"
|
|
263
|
+
local default_choice="keep_branch"
|
|
264
|
+
local timeout=900 # 15 minutes in seconds
|
|
265
|
+
local elapsed=0
|
|
266
|
+
|
|
267
|
+
# Read default from action file if present
|
|
268
|
+
if [ -f "$action_file" ]; then
|
|
269
|
+
local parsed_default
|
|
270
|
+
parsed_default=$(node -e "try { const d=require('fs').readFileSync(process.argv[1],'utf8'); console.log(JSON.parse(d).default||'keep_branch'); } catch(e) { console.log('keep_branch'); }" "$action_file" 2>/dev/null || echo "keep_branch")
|
|
271
|
+
if [ -n "$parsed_default" ]; then
|
|
272
|
+
default_choice="$parsed_default"
|
|
273
|
+
fi
|
|
274
|
+
fi
|
|
275
|
+
|
|
276
|
+
while [ $elapsed -lt $timeout ]; do
|
|
277
|
+
if [ -f "$reply_file" ]; then
|
|
278
|
+
local choice
|
|
279
|
+
choice=$(node -e "try { const d=require('fs').readFileSync(process.argv[1],'utf8'); console.log(JSON.parse(d).choice||''); } catch(e) { console.log(''); }" "$reply_file" 2>/dev/null || echo "")
|
|
280
|
+
if [ -n "$choice" ]; then
|
|
281
|
+
echo "User selected: $choice" >&2
|
|
282
|
+
# Cleanup both files
|
|
283
|
+
rm -f "$action_file" "$reply_file" 2>/dev/null || true
|
|
284
|
+
echo "$choice"
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
fi
|
|
288
|
+
sleep 1
|
|
289
|
+
elapsed=$((elapsed + 1))
|
|
290
|
+
done
|
|
291
|
+
|
|
292
|
+
# Timeout: use default
|
|
293
|
+
echo "Action reply timeout after ${timeout}s, using default: $default_choice" >&2
|
|
294
|
+
rm -f "$action_file" "$reply_file" 2>/dev/null || true
|
|
295
|
+
echo "$default_choice"
|
|
139
296
|
}
|
|
140
297
|
|
|
141
298
|
# Initialize tokens
|
|
@@ -237,12 +394,13 @@ if [ ! -f "$PLAN_FILE" ]; then
|
|
|
237
394
|
echo "======================== PLANNING PHASE ========================"
|
|
238
395
|
write_phase_start "planning"
|
|
239
396
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
240
|
-
cat "$PROMPTS_DIR/PROMPT_feature.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT" || {
|
|
397
|
+
cat "$PROMPTS_DIR/PROMPT_feature.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || {
|
|
241
398
|
echo "ERROR: Planning phase failed"
|
|
242
399
|
write_phase_end "planning" "failed"
|
|
243
400
|
exit 1
|
|
244
401
|
}
|
|
245
|
-
|
|
402
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
403
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
246
404
|
write_phase_end "planning" "success"
|
|
247
405
|
else
|
|
248
406
|
echo "Plan file exists, skipping planning phase"
|
|
@@ -275,8 +433,9 @@ while true; do
|
|
|
275
433
|
|
|
276
434
|
echo "Pending implementation tasks: $PENDING_IMPL"
|
|
277
435
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
278
|
-
cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
279
|
-
|
|
436
|
+
cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
437
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
438
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
280
439
|
|
|
281
440
|
sleep 2
|
|
282
441
|
done
|
|
@@ -300,8 +459,9 @@ else
|
|
|
300
459
|
echo "------------------------ E2E Attempt $E2E_ATTEMPT of $MAX_E2E_ATTEMPTS ------------------------"
|
|
301
460
|
|
|
302
461
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
303
|
-
cat "$PROMPTS_DIR/PROMPT_e2e.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
304
|
-
|
|
462
|
+
cat "$PROMPTS_DIR/PROMPT_e2e.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
463
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
464
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
305
465
|
|
|
306
466
|
# Check if all E2E tests passed
|
|
307
467
|
E2E_FAILED=$({ grep "^- \[ \].*E2E:.*FAILED" "$PLAN_FILE" 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
@@ -315,8 +475,9 @@ else
|
|
|
315
475
|
|
|
316
476
|
if [ $E2E_ATTEMPT -lt $MAX_E2E_ATTEMPTS ]; then
|
|
317
477
|
echo "E2E tests have failures. Running fix iteration..."
|
|
318
|
-
cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
319
|
-
|
|
478
|
+
cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
479
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
480
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
320
481
|
fi
|
|
321
482
|
done
|
|
322
483
|
|
|
@@ -332,10 +493,11 @@ echo "======================== SPEC VERIFICATION PHASE ========================"
|
|
|
332
493
|
write_phase_start "verification"
|
|
333
494
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
334
495
|
VERIFY_STATUS="success"
|
|
335
|
-
if ! cat "$PROMPTS_DIR/PROMPT_verify.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"; then
|
|
496
|
+
if ! cat "$PROMPTS_DIR/PROMPT_verify.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw"; then
|
|
336
497
|
VERIFY_STATUS="failed"
|
|
337
498
|
fi
|
|
338
|
-
|
|
499
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
500
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
339
501
|
write_phase_end "verification" "$VERIFY_STATUS"
|
|
340
502
|
|
|
341
503
|
# Phase 7: PR and Review
|
|
@@ -343,18 +505,136 @@ echo "======================== PR & REVIEW PHASE ========================"
|
|
|
343
505
|
write_phase_start "pr_review"
|
|
344
506
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
345
507
|
PR_STATUS="success"
|
|
508
|
+
MAX_REVIEW_ATTEMPTS=3
|
|
509
|
+
|
|
510
|
+
# Check for review approval in stdout or latest PR comment.
|
|
511
|
+
# Returns 0 (true) if approved, 1 (false) otherwise.
|
|
512
|
+
check_review_approved() {
|
|
513
|
+
local output_file="$1"
|
|
514
|
+
|
|
515
|
+
# Primary: check stdout for explicit verdict line
|
|
516
|
+
if grep -qi "VERDICT:.*APPROVED" "$output_file" 2>/dev/null; then
|
|
517
|
+
# Make sure it's not "NOT APPROVED"
|
|
518
|
+
if ! grep -qi "VERDICT:.*NOT APPROVED" "$output_file" 2>/dev/null; then
|
|
519
|
+
return 0
|
|
520
|
+
fi
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
# Fallback: check the latest PR comment for approval signal
|
|
524
|
+
local latest_comment
|
|
525
|
+
latest_comment=$(gh pr view "$BRANCH" --json comments --jq '.comments[-1].body' 2>/dev/null || echo "")
|
|
526
|
+
if echo "$latest_comment" | grep -qi "VERDICT:.*APPROVED\|Verdict:.*APPROVED" 2>/dev/null; then
|
|
527
|
+
if ! echo "$latest_comment" | grep -qi "NOT APPROVED" 2>/dev/null; then
|
|
528
|
+
return 0
|
|
529
|
+
fi
|
|
530
|
+
fi
|
|
531
|
+
|
|
532
|
+
return 1
|
|
533
|
+
}
|
|
534
|
+
|
|
346
535
|
if [ "$REVIEW_MODE" = "manual" ]; then
|
|
347
|
-
if ! cat "$PROMPTS_DIR/PROMPT_review_manual.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT"; then
|
|
536
|
+
if ! cat "$PROMPTS_DIR/PROMPT_review_manual.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw"; then
|
|
348
537
|
PR_STATUS="failed"
|
|
349
538
|
fi
|
|
350
|
-
|
|
351
|
-
|
|
539
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
540
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
541
|
+
|
|
542
|
+
elif [ "$REVIEW_MODE" = "merge" ]; then
|
|
543
|
+
# Merge mode: create PR, iterate review+fixes until approved, then merge
|
|
544
|
+
REVIEW_ATTEMPT=0
|
|
545
|
+
REVIEW_APPROVED=false
|
|
546
|
+
while [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; do
|
|
547
|
+
REVIEW_ATTEMPT=$((REVIEW_ATTEMPT + 1))
|
|
548
|
+
echo "--- Review attempt $REVIEW_ATTEMPT of $MAX_REVIEW_ATTEMPTS ---"
|
|
549
|
+
cat "$PROMPTS_DIR/PROMPT_review_merge.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
550
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
551
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
552
|
+
|
|
553
|
+
# Check stdout and PR comment for approval
|
|
554
|
+
if check_review_approved "${CLAUDE_OUTPUT}.raw"; then
|
|
555
|
+
echo "Review approved!"
|
|
556
|
+
REVIEW_APPROVED=true
|
|
557
|
+
break
|
|
558
|
+
fi
|
|
559
|
+
|
|
560
|
+
if [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; then
|
|
561
|
+
echo "Review found issues. Running fix iteration..."
|
|
562
|
+
echo "Fix the issues found in the code review above. Run git diff main to see the current changes, then:
|
|
563
|
+
1. Fix each issue referenced in the review
|
|
564
|
+
2. Run tests: npm test
|
|
565
|
+
3. Commit and push the fixes
|
|
566
|
+
Do NOT propose completion options or ask interactive questions. Just fix, test, commit, push." | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
567
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
568
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
569
|
+
fi
|
|
570
|
+
done
|
|
571
|
+
if [ "$REVIEW_APPROVED" != true ]; then
|
|
572
|
+
echo "Review not approved after $MAX_REVIEW_ATTEMPTS attempts."
|
|
352
573
|
PR_STATUS="failed"
|
|
353
574
|
fi
|
|
575
|
+
|
|
576
|
+
else
|
|
577
|
+
# Auto mode: create PR, iterate review+fixes until approved (no merge)
|
|
578
|
+
REVIEW_ATTEMPT=0
|
|
579
|
+
REVIEW_APPROVED=false
|
|
580
|
+
while [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; do
|
|
581
|
+
REVIEW_ATTEMPT=$((REVIEW_ATTEMPT + 1))
|
|
582
|
+
echo "--- Review attempt $REVIEW_ATTEMPT of $MAX_REVIEW_ATTEMPTS ---"
|
|
583
|
+
cat "$PROMPTS_DIR/PROMPT_review_auto.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
584
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
585
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
586
|
+
|
|
587
|
+
# Check stdout and PR comment for approval
|
|
588
|
+
if check_review_approved "${CLAUDE_OUTPUT}.raw"; then
|
|
589
|
+
echo "Review approved!"
|
|
590
|
+
REVIEW_APPROVED=true
|
|
591
|
+
break
|
|
592
|
+
fi
|
|
593
|
+
|
|
594
|
+
if [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; then
|
|
595
|
+
echo "Review found issues. Running fix iteration..."
|
|
596
|
+
echo "Fix the issues found in the code review above. Run git diff main to see the current changes, then:
|
|
597
|
+
1. Fix each issue referenced in the review
|
|
598
|
+
2. Run tests: npm test
|
|
599
|
+
3. Commit and push the fixes
|
|
600
|
+
Do NOT propose completion options or ask interactive questions. Just fix, test, commit, push." | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
|
|
601
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
602
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
603
|
+
fi
|
|
604
|
+
done
|
|
605
|
+
if [ "$REVIEW_APPROVED" != true ]; then
|
|
606
|
+
echo "Review not approved after $MAX_REVIEW_ATTEMPTS attempts. PR ready for manual review."
|
|
607
|
+
fi
|
|
354
608
|
fi
|
|
355
|
-
parse_and_accumulate_tokens "$CLAUDE_OUTPUT"
|
|
356
609
|
write_phase_end "pr_review" "$PR_STATUS"
|
|
357
610
|
|
|
611
|
+
# Phase 7.5: Post-completion action request
|
|
612
|
+
echo "======================== ACTION REQUEST PHASE ========================"
|
|
613
|
+
write_action_request
|
|
614
|
+
CHOSEN_ACTION=$(poll_action_reply)
|
|
615
|
+
echo "User chose: $CHOSEN_ACTION"
|
|
616
|
+
|
|
617
|
+
# Dispatch based on user choice
|
|
618
|
+
case "$CHOSEN_ACTION" in
|
|
619
|
+
done)
|
|
620
|
+
echo "Loop complete. Exiting."
|
|
621
|
+
;;
|
|
622
|
+
merge_local)
|
|
623
|
+
echo "Merging back to main locally..."
|
|
624
|
+
git checkout main 2>/dev/null || git checkout master
|
|
625
|
+
git merge --squash "$BRANCH" && git commit -m "feat($FEATURE): squash merge from $BRANCH"
|
|
626
|
+
echo "Merged. You can delete the branch with: git branch -D $BRANCH"
|
|
627
|
+
;;
|
|
628
|
+
discard)
|
|
629
|
+
echo "Discarding work on branch $BRANCH..."
|
|
630
|
+
git checkout main 2>/dev/null || git checkout master
|
|
631
|
+
git branch -D "$BRANCH" 2>/dev/null || echo "Branch $BRANCH not found locally."
|
|
632
|
+
;;
|
|
633
|
+
keep_branch|*)
|
|
634
|
+
echo "Keeping branch $BRANCH as-is."
|
|
635
|
+
;;
|
|
636
|
+
esac
|
|
637
|
+
|
|
358
638
|
# Persist final status for TUI summaries
|
|
359
639
|
if ! echo "$ITERATION|$MAX_ITERATIONS|$(date +%s)|done" > "$FINAL_STATUS_FILE"; then
|
|
360
640
|
echo "WARNING: Failed to write final status file: $FINAL_STATUS_FILE" >&2
|
|
@@ -363,16 +643,30 @@ fi
|
|
|
363
643
|
# Cleanup temp files
|
|
364
644
|
rm -f "$STATUS_FILE" 2>/dev/null || true
|
|
365
645
|
rm -f "/tmp/ralph-loop-${FEATURE}.output" 2>/dev/null || true
|
|
646
|
+
rm -f "/tmp/ralph-loop-${FEATURE}.output.raw" 2>/dev/null || true
|
|
366
647
|
|
|
367
648
|
# Print final token usage
|
|
368
649
|
if [ -f "/tmp/ralph-loop-${FEATURE}.tokens" ]; then
|
|
369
650
|
echo ""
|
|
370
651
|
echo "=========================================="
|
|
371
652
|
echo "Final Token Usage:"
|
|
372
|
-
|
|
653
|
+
awk -F'|' '{
|
|
654
|
+
printf " Input: %d tokens\n", $1
|
|
655
|
+
printf " Output: %d tokens\n", $2
|
|
656
|
+
printf " Cache create: %d tokens\n", $3
|
|
657
|
+
printf " Cache read: %d tokens\n", $4
|
|
658
|
+
printf " Total: %d tokens\n", $1+$2+$3+$4
|
|
659
|
+
}' "/tmp/ralph-loop-${FEATURE}.tokens"
|
|
373
660
|
echo "=========================================="
|
|
374
661
|
fi
|
|
375
662
|
|
|
663
|
+
# Print session IDs
|
|
664
|
+
if [ -f "/tmp/ralph-loop-${FEATURE}.sessions" ]; then
|
|
665
|
+
SESSION_COUNT=$(wc -l < "/tmp/ralph-loop-${FEATURE}.sessions" | tr -d ' ')
|
|
666
|
+
echo "Sessions: $SESSION_COUNT"
|
|
667
|
+
cat "/tmp/ralph-loop-${FEATURE}.sessions"
|
|
668
|
+
fi
|
|
669
|
+
|
|
376
670
|
echo "=========================================="
|
|
377
671
|
echo "Ralph loop completed: $FEATURE"
|
|
378
672
|
echo "=========================================="
|