wiggum-cli 0.15.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 +7 -1
- 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 +222 -40
- 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 +7 -2
- package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +7 -2
- package/dist/templates/scripts/feature-loop.sh.tmpl +190 -46
- package/dist/tui/app.d.ts +16 -1
- package/dist/tui/app.js +11 -3
- package/dist/tui/components/ActivityFeed.d.ts +18 -0
- package/dist/tui/components/ActivityFeed.js +31 -0
- package/dist/tui/components/RunCompletionSummary.d.ts +27 -1
- package/dist/tui/components/RunCompletionSummary.js +97 -7
- package/dist/tui/components/SummaryBox.d.ts +4 -0
- package/dist/tui/components/SummaryBox.js +4 -2
- package/dist/tui/hooks/useBackgroundRuns.js +1 -1
- package/dist/tui/screens/RunScreen.d.ts +15 -15
- package/dist/tui/screens/RunScreen.js +58 -5
- 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/utils/ci.d.ts +8 -0
- package/dist/utils/ci.js +13 -0
- package/dist/utils/spec-names.js +5 -1
- package/package.json +7 -2
- 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 +7 -2
- package/src/templates/prompts/PROMPT_review_merge.md.tmpl +7 -2
- package/src/templates/scripts/feature-loop.sh.tmpl +190 -46
|
@@ -97,8 +97,8 @@ if [ "$REVIEW_MODE" != "manual" ] && [ "$REVIEW_MODE" != "auto" ] && [ "$REVIEW_
|
|
|
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,130 @@ 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
|
|
142
|
+
|
|
143
|
+
LAST_SESSION_ID="$result"
|
|
144
|
+
|
|
145
|
+
if [ -n "$LAST_SESSION_ID" ]; then
|
|
146
|
+
echo "$LAST_SESSION_ID" >> "$SESSIONS_FILE"
|
|
147
|
+
fi
|
|
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
|
|
121
179
|
|
|
122
|
-
|
|
123
|
-
|
|
180
|
+
if [ -z "$jsonl_file" ]; then
|
|
181
|
+
echo "WARNING: Could not find JSONL for session $session_id" >&2
|
|
182
|
+
return
|
|
183
|
+
fi
|
|
124
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
|
-
current_output=0
|
|
229
|
+
c_input=0; c_output=0; c_cache_create=0; c_cache_read=0
|
|
134
230
|
fi
|
|
135
231
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
echo "${new_input}|${new_output}" > "$TOKENS_FILE"
|
|
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"
|
|
139
234
|
}
|
|
140
235
|
|
|
141
236
|
# Action inbox: write request file if not already present
|
|
@@ -299,12 +394,13 @@ if [ ! -f "$PLAN_FILE" ]; then
|
|
|
299
394
|
echo "======================== PLANNING PHASE ========================"
|
|
300
395
|
write_phase_start "planning"
|
|
301
396
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
302
|
-
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" || {
|
|
303
398
|
echo "ERROR: Planning phase failed"
|
|
304
399
|
write_phase_end "planning" "failed"
|
|
305
400
|
exit 1
|
|
306
401
|
}
|
|
307
|
-
|
|
402
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
403
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
308
404
|
write_phase_end "planning" "success"
|
|
309
405
|
else
|
|
310
406
|
echo "Plan file exists, skipping planning phase"
|
|
@@ -337,8 +433,9 @@ while true; do
|
|
|
337
433
|
|
|
338
434
|
echo "Pending implementation tasks: $PENDING_IMPL"
|
|
339
435
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
340
|
-
cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
341
|
-
|
|
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"
|
|
342
439
|
|
|
343
440
|
sleep 2
|
|
344
441
|
done
|
|
@@ -362,8 +459,9 @@ else
|
|
|
362
459
|
echo "------------------------ E2E Attempt $E2E_ATTEMPT of $MAX_E2E_ATTEMPTS ------------------------"
|
|
363
460
|
|
|
364
461
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
365
|
-
cat "$PROMPTS_DIR/PROMPT_e2e.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
366
|
-
|
|
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"
|
|
367
465
|
|
|
368
466
|
# Check if all E2E tests passed
|
|
369
467
|
E2E_FAILED=$({ grep "^- \[ \].*E2E:.*FAILED" "$PLAN_FILE" 2>/dev/null || true; } | wc -l | tr -d ' ')
|
|
@@ -377,8 +475,9 @@ else
|
|
|
377
475
|
|
|
378
476
|
if [ $E2E_ATTEMPT -lt $MAX_E2E_ATTEMPTS ]; then
|
|
379
477
|
echo "E2E tests have failures. Running fix iteration..."
|
|
380
|
-
cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
381
|
-
|
|
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"
|
|
382
481
|
fi
|
|
383
482
|
done
|
|
384
483
|
|
|
@@ -394,10 +493,11 @@ echo "======================== SPEC VERIFICATION PHASE ========================"
|
|
|
394
493
|
write_phase_start "verification"
|
|
395
494
|
export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
396
495
|
VERIFY_STATUS="success"
|
|
397
|
-
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
|
|
398
497
|
VERIFY_STATUS="failed"
|
|
399
498
|
fi
|
|
400
|
-
|
|
499
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
500
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
401
501
|
write_phase_end "verification" "$VERIFY_STATUS"
|
|
402
502
|
|
|
403
503
|
# Phase 7: PR and Review
|
|
@@ -407,11 +507,37 @@ export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
|
|
|
407
507
|
PR_STATUS="success"
|
|
408
508
|
MAX_REVIEW_ATTEMPTS=3
|
|
409
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
|
+
|
|
410
535
|
if [ "$REVIEW_MODE" = "manual" ]; then
|
|
411
|
-
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
|
|
412
537
|
PR_STATUS="failed"
|
|
413
538
|
fi
|
|
414
|
-
|
|
539
|
+
extract_session_result "${CLAUDE_OUTPUT}.raw"
|
|
540
|
+
accumulate_tokens_from_session "$LAST_SESSION_ID"
|
|
415
541
|
|
|
416
542
|
elif [ "$REVIEW_MODE" = "merge" ]; then
|
|
417
543
|
# Merge mode: create PR, iterate review+fixes until approved, then merge
|
|
@@ -420,11 +546,12 @@ elif [ "$REVIEW_MODE" = "merge" ]; then
|
|
|
420
546
|
while [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; do
|
|
421
547
|
REVIEW_ATTEMPT=$((REVIEW_ATTEMPT + 1))
|
|
422
548
|
echo "--- Review attempt $REVIEW_ATTEMPT of $MAX_REVIEW_ATTEMPTS ---"
|
|
423
|
-
cat "$PROMPTS_DIR/PROMPT_review_merge.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
424
|
-
|
|
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"
|
|
425
552
|
|
|
426
|
-
# Check
|
|
427
|
-
if
|
|
553
|
+
# Check stdout and PR comment for approval
|
|
554
|
+
if check_review_approved "${CLAUDE_OUTPUT}.raw"; then
|
|
428
555
|
echo "Review approved!"
|
|
429
556
|
REVIEW_APPROVED=true
|
|
430
557
|
break
|
|
@@ -436,8 +563,9 @@ elif [ "$REVIEW_MODE" = "merge" ]; then
|
|
|
436
563
|
1. Fix each issue referenced in the review
|
|
437
564
|
2. Run tests: npm test
|
|
438
565
|
3. Commit and push the fixes
|
|
439
|
-
Do NOT propose completion options or ask interactive questions. Just fix, test, commit, push." | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
440
|
-
|
|
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"
|
|
441
569
|
fi
|
|
442
570
|
done
|
|
443
571
|
if [ "$REVIEW_APPROVED" != true ]; then
|
|
@@ -452,11 +580,12 @@ else
|
|
|
452
580
|
while [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; do
|
|
453
581
|
REVIEW_ATTEMPT=$((REVIEW_ATTEMPT + 1))
|
|
454
582
|
echo "--- Review attempt $REVIEW_ATTEMPT of $MAX_REVIEW_ATTEMPTS ---"
|
|
455
|
-
cat "$PROMPTS_DIR/PROMPT_review_auto.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
456
|
-
|
|
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"
|
|
457
586
|
|
|
458
|
-
# Check
|
|
459
|
-
if
|
|
587
|
+
# Check stdout and PR comment for approval
|
|
588
|
+
if check_review_approved "${CLAUDE_OUTPUT}.raw"; then
|
|
460
589
|
echo "Review approved!"
|
|
461
590
|
REVIEW_APPROVED=true
|
|
462
591
|
break
|
|
@@ -468,8 +597,9 @@ else
|
|
|
468
597
|
1. Fix each issue referenced in the review
|
|
469
598
|
2. Run tests: npm test
|
|
470
599
|
3. Commit and push the fixes
|
|
471
|
-
Do NOT propose completion options or ask interactive questions. Just fix, test, commit, push." | $CLAUDE_CMD_IMPL 2>&1 | tee "$CLAUDE_OUTPUT" || true
|
|
472
|
-
|
|
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"
|
|
473
603
|
fi
|
|
474
604
|
done
|
|
475
605
|
if [ "$REVIEW_APPROVED" != true ]; then
|
|
@@ -513,16 +643,30 @@ fi
|
|
|
513
643
|
# Cleanup temp files
|
|
514
644
|
rm -f "$STATUS_FILE" 2>/dev/null || true
|
|
515
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
|
|
516
647
|
|
|
517
648
|
# Print final token usage
|
|
518
649
|
if [ -f "/tmp/ralph-loop-${FEATURE}.tokens" ]; then
|
|
519
650
|
echo ""
|
|
520
651
|
echo "=========================================="
|
|
521
652
|
echo "Final Token Usage:"
|
|
522
|
-
|
|
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"
|
|
523
660
|
echo "=========================================="
|
|
524
661
|
fi
|
|
525
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
|
+
|
|
526
670
|
echo "=========================================="
|
|
527
671
|
echo "Ralph loop completed: $FEATURE"
|
|
528
672
|
echo "=========================================="
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -32,6 +32,17 @@ export interface InterviewAppProps {
|
|
|
32
32
|
/** Optional scan result with detected tech stack */
|
|
33
33
|
scanResult?: ScanResult;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Props for the run/monitor screen when launched directly from CLI
|
|
37
|
+
*/
|
|
38
|
+
export interface RunAppProps {
|
|
39
|
+
/** Name of the feature to run or monitor */
|
|
40
|
+
featureName: string;
|
|
41
|
+
/** If true, opens in monitor-only (read-only) mode — no loop is spawned */
|
|
42
|
+
monitorOnly?: boolean;
|
|
43
|
+
/** Review mode override */
|
|
44
|
+
reviewMode?: 'manual' | 'auto';
|
|
45
|
+
}
|
|
35
46
|
/**
|
|
36
47
|
* Props for the main App component
|
|
37
48
|
*/
|
|
@@ -44,6 +55,8 @@ export interface AppProps {
|
|
|
44
55
|
version?: string;
|
|
45
56
|
/** Props for the interview screen (required when screen is 'interview') */
|
|
46
57
|
interviewProps?: InterviewAppProps;
|
|
58
|
+
/** Props for the run/monitor screen (required when screen is 'run') */
|
|
59
|
+
runProps?: RunAppProps;
|
|
47
60
|
/** Called when the screen completes successfully */
|
|
48
61
|
onComplete?: (result: string) => void;
|
|
49
62
|
/** Called when the user exits/cancels */
|
|
@@ -56,7 +69,7 @@ export interface AppProps {
|
|
|
56
69
|
* and receives a shared headerElement prop.
|
|
57
70
|
*/
|
|
58
71
|
export declare function App({ screen: initialScreen, initialSessionState, version, // Fallback if package.json read fails (keep in sync with index.ts)
|
|
59
|
-
interviewProps, onComplete, onExit, }: AppProps): React.ReactElement | null;
|
|
72
|
+
interviewProps, runProps, onComplete, onExit, }: AppProps): React.ReactElement | null;
|
|
60
73
|
/**
|
|
61
74
|
* Render options for renderApp
|
|
62
75
|
*/
|
|
@@ -69,6 +82,8 @@ export interface RenderAppOptions {
|
|
|
69
82
|
version?: string;
|
|
70
83
|
/** Props for interview screen (if starting directly on interview) */
|
|
71
84
|
interviewProps?: InterviewAppProps;
|
|
85
|
+
/** Props for run/monitor screen (if starting directly on run screen) */
|
|
86
|
+
runProps?: RunAppProps;
|
|
72
87
|
/** Called when spec generation completes */
|
|
73
88
|
onComplete?: (result: string) => void;
|
|
74
89
|
/** Called when user exits */
|
package/dist/tui/app.js
CHANGED
|
@@ -29,9 +29,17 @@ import { useBackgroundRuns } from './hooks/useBackgroundRuns.js';
|
|
|
29
29
|
* and receives a shared headerElement prop.
|
|
30
30
|
*/
|
|
31
31
|
export function App({ screen: initialScreen, initialSessionState, version = '0.12.1', // Fallback if package.json read fails (keep in sync with index.ts)
|
|
32
|
-
interviewProps, onComplete, onExit, }) {
|
|
32
|
+
interviewProps, runProps, onComplete, onExit, }) {
|
|
33
33
|
const [currentScreen, setCurrentScreen] = useState(initialScreen);
|
|
34
|
-
const [screenProps, setScreenProps] = useState(
|
|
34
|
+
const [screenProps, setScreenProps] = useState(() => {
|
|
35
|
+
if (initialScreen === 'run' && runProps) {
|
|
36
|
+
return { featureName: runProps.featureName, monitorOnly: runProps.monitorOnly, reviewMode: runProps.reviewMode };
|
|
37
|
+
}
|
|
38
|
+
if (interviewProps) {
|
|
39
|
+
return { featureName: interviewProps.featureName };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
});
|
|
35
43
|
const [sessionState, setSessionState] = useState(initialSessionState);
|
|
36
44
|
// Background run tracking
|
|
37
45
|
const { runs: backgroundRuns, background, dismiss } = useBackgroundRuns();
|
|
@@ -216,5 +224,5 @@ interviewProps, onComplete, onExit, }) {
|
|
|
216
224
|
* Render the App component to the terminal
|
|
217
225
|
*/
|
|
218
226
|
export function renderApp(options) {
|
|
219
|
-
return render(_jsx(App, { screen: options.screen, initialSessionState: options.initialSessionState, version: options.version, interviewProps: options.interviewProps, onComplete: options.onComplete, onExit: options.onExit }));
|
|
227
|
+
return render(_jsx(App, { screen: options.screen, initialSessionState: options.initialSessionState, version: options.version, interviewProps: options.interviewProps, runProps: options.runProps, onComplete: options.onComplete, onExit: options.onExit }));
|
|
220
228
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ActivityFeed - Live activity feed for RunScreen
|
|
3
|
+
*
|
|
4
|
+
* Renders the last N activity events derived from the loop log and phase changes.
|
|
5
|
+
* Each row shows a relative timestamp, a status icon, and the event message.
|
|
6
|
+
* Color-coded by status: green (success), red/pink (error), yellow (in-progress).
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { type ActivityEvent } from '../utils/loop-status.js';
|
|
10
|
+
export interface ActivityFeedProps {
|
|
11
|
+
/** Activity events to display (newest last) */
|
|
12
|
+
events: ActivityEvent[];
|
|
13
|
+
/** Maximum number of events to show (default: 10) */
|
|
14
|
+
maxEvents?: number;
|
|
15
|
+
/** Commit range string to display as footer (e.g., "b425c40 → 6efaf80") */
|
|
16
|
+
latestCommit?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare function ActivityFeed({ events, maxEvents, latestCommit }: ActivityFeedProps): React.ReactElement;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { colors, phase } from '../theme.js';
|
|
4
|
+
import { formatRelativeTime } from '../utils/loop-status.js';
|
|
5
|
+
const MAX_MESSAGE_LENGTH = 90;
|
|
6
|
+
const STATUS_COLOR = {
|
|
7
|
+
success: colors.green,
|
|
8
|
+
error: colors.pink,
|
|
9
|
+
'in-progress': colors.yellow,
|
|
10
|
+
};
|
|
11
|
+
const STATUS_ICON = {
|
|
12
|
+
success: phase.complete, // ✓
|
|
13
|
+
error: phase.error, // ✗
|
|
14
|
+
'in-progress': phase.active, // ◐
|
|
15
|
+
};
|
|
16
|
+
function truncateMessage(message, maxLen) {
|
|
17
|
+
if (message.length <= maxLen)
|
|
18
|
+
return message;
|
|
19
|
+
return message.slice(0, maxLen - 1) + '\u2026';
|
|
20
|
+
}
|
|
21
|
+
export function ActivityFeed({ events, maxEvents = 10, latestCommit }) {
|
|
22
|
+
const visible = events.slice(-maxEvents);
|
|
23
|
+
if (visible.length === 0) {
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No activity yet" }), latestCommit && _jsxs(Text, { dimColor: true, children: ["Commit: ", latestCommit] })] }));
|
|
25
|
+
}
|
|
26
|
+
return (_jsxs(Box, { flexDirection: "column", children: [visible.map((event, idx) => {
|
|
27
|
+
const color = STATUS_COLOR[event.status];
|
|
28
|
+
const icon = STATUS_ICON[event.status];
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(Text, { dimColor: true, children: formatRelativeTime(event.timestamp) }), _jsx(Text, { color: color, children: icon }), _jsx(Text, { color: color, children: truncateMessage(event.message, MAX_MESSAGE_LENGTH) })] }, idx));
|
|
30
|
+
}), latestCommit && _jsxs(Text, { dimColor: true, children: ["Commit: ", latestCommit] })] }));
|
|
31
|
+
}
|
|
@@ -5,7 +5,33 @@
|
|
|
5
5
|
* commits, and PR/issue links after a feature loop completes.
|
|
6
6
|
*/
|
|
7
7
|
import React from 'react';
|
|
8
|
-
import type { RunSummary } from '../screens/RunScreen.js';
|
|
8
|
+
import type { RunSummary, FileChangeStat } from '../screens/RunScreen.js';
|
|
9
|
+
/**
|
|
10
|
+
* Formatted file change row for aligned display in the Changes section.
|
|
11
|
+
*/
|
|
12
|
+
export interface FormattedFileChange {
|
|
13
|
+
/** Path, possibly truncated with prefix ellipsis, padded to path column width */
|
|
14
|
+
displayPath: string;
|
|
15
|
+
/** Insertions string, e.g. "+10" or "+ 3", right-aligned within fixed width */
|
|
16
|
+
addedStr: string;
|
|
17
|
+
/** Deletions string, e.g. "-5" or "-18", right-aligned within fixed width */
|
|
18
|
+
removedStr: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Truncate a file path to fit within maxWidth characters.
|
|
22
|
+
* If the path exceeds maxWidth, truncates from the start and prefixes with '…'.
|
|
23
|
+
*/
|
|
24
|
+
export declare function truncatePath(path: string, maxWidth: number): string;
|
|
25
|
+
/**
|
|
26
|
+
* Truncate text from the end to fit within maxWidth characters.
|
|
27
|
+
* Appends '…' if truncated.
|
|
28
|
+
*/
|
|
29
|
+
export declare function truncateEnd(text: string, maxWidth: number): string;
|
|
30
|
+
/**
|
|
31
|
+
* Format a list of file change stats into fixed-width, aligned display rows.
|
|
32
|
+
* Stats columns are always fully visible; the path column shrinks to fill remaining space.
|
|
33
|
+
*/
|
|
34
|
+
export declare function formatChangesFiles(files: FileChangeStat[], contentWidth: number): FormattedFileChange[];
|
|
9
35
|
/**
|
|
10
36
|
* Props for RunCompletionSummary component
|
|
11
37
|
*/
|
|
@@ -1,7 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import { SummaryBox, SummaryBoxSection } from './SummaryBox.js';
|
|
2
|
+
import { Box, Text, useStdout } from 'ink';
|
|
3
|
+
import { SummaryBox, SummaryBoxSection, MAX_BOX_WIDTH, BOX_BORDER_OVERHEAD } from './SummaryBox.js';
|
|
4
4
|
import { colors, phase } from '../theme.js';
|
|
5
|
+
import { formatNumber } from '../utils/loop-status.js';
|
|
6
|
+
const MIN_BOX_WIDTH = 60;
|
|
7
|
+
/**
|
|
8
|
+
* Truncate a file path to fit within maxWidth characters.
|
|
9
|
+
* If the path exceeds maxWidth, truncates from the start and prefixes with '…'.
|
|
10
|
+
*/
|
|
11
|
+
export function truncatePath(path, maxWidth) {
|
|
12
|
+
if (path.length <= maxWidth)
|
|
13
|
+
return path;
|
|
14
|
+
if (maxWidth <= 1)
|
|
15
|
+
return '…';
|
|
16
|
+
return '…' + path.slice(-(maxWidth - 1));
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Truncate text from the end to fit within maxWidth characters.
|
|
20
|
+
* Appends '…' if truncated.
|
|
21
|
+
*/
|
|
22
|
+
export function truncateEnd(text, maxWidth) {
|
|
23
|
+
if (text.length <= maxWidth)
|
|
24
|
+
return text;
|
|
25
|
+
if (maxWidth <= 1)
|
|
26
|
+
return '…';
|
|
27
|
+
return text.slice(0, maxWidth - 1) + '…';
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Format a list of file change stats into fixed-width, aligned display rows.
|
|
31
|
+
* Stats columns are always fully visible; the path column shrinks to fill remaining space.
|
|
32
|
+
*/
|
|
33
|
+
export function formatChangesFiles(files, contentWidth) {
|
|
34
|
+
if (files.length === 0)
|
|
35
|
+
return [];
|
|
36
|
+
const GAP = 2;
|
|
37
|
+
const maxAddedDigits = Math.max(...files.map((f) => String(f.added).length));
|
|
38
|
+
const maxRemovedDigits = Math.max(...files.map((f) => String(f.removed).length));
|
|
39
|
+
// statsBlockWidth: "+NNN -NNN"
|
|
40
|
+
const statsBlockWidth = (1 + maxAddedDigits) + 1 + (1 + maxRemovedDigits);
|
|
41
|
+
const pathColWidth = Math.max(1, contentWidth - GAP - statsBlockWidth);
|
|
42
|
+
return files.map((file) => ({
|
|
43
|
+
displayPath: truncatePath(file.path, pathColWidth).padEnd(pathColWidth),
|
|
44
|
+
addedStr: '+' + String(file.added).padStart(maxAddedDigits),
|
|
45
|
+
removedStr: '-' + String(file.removed).padStart(maxRemovedDigits),
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
5
48
|
/**
|
|
6
49
|
* Format milliseconds to human-readable duration (e.g., "12m 34s", "1h 15m 0s")
|
|
7
50
|
*/
|
|
@@ -26,6 +69,10 @@ function formatDurationMs(ms) {
|
|
|
26
69
|
*/
|
|
27
70
|
const stoppedCodes = new Set([130, 143]);
|
|
28
71
|
export function RunCompletionSummary({ summary, }) {
|
|
72
|
+
const { stdout } = useStdout();
|
|
73
|
+
const terminalWidth = stdout?.columns ?? 80;
|
|
74
|
+
const boxWidth = Math.min(Math.max(MIN_BOX_WIDTH, terminalWidth), MAX_BOX_WIDTH);
|
|
75
|
+
const contentWidth = boxWidth - BOX_BORDER_OVERHEAD;
|
|
29
76
|
// Determine final status and color
|
|
30
77
|
const exitStatus = summary.exitCode === 0
|
|
31
78
|
? { label: 'Complete', color: colors.green }
|
|
@@ -48,11 +95,29 @@ export function RunCompletionSummary({ summary, }) {
|
|
|
48
95
|
const tasksDisplay = tasksCompleted !== null && tasksTotal !== null
|
|
49
96
|
? `${tasksCompleted}/${tasksTotal} completed`
|
|
50
97
|
: 'Not available';
|
|
51
|
-
|
|
52
|
-
|
|
98
|
+
// Status icon for subtitle
|
|
99
|
+
const statusIcon = summary.exitCode === 0
|
|
100
|
+
? phase.complete
|
|
101
|
+
: stoppedCodes.has(summary.exitCode) ? phase.active : phase.error;
|
|
102
|
+
// Total tokens across all categories
|
|
103
|
+
const totalTokens = summary.tokensInput + summary.tokensOutput + summary.cacheCreate + summary.cacheRead;
|
|
104
|
+
// Build compact subtitle parts
|
|
105
|
+
const subtitleParts = [];
|
|
106
|
+
if (summary.totalDurationMs !== undefined) {
|
|
107
|
+
subtitleParts.push(formatDurationMs(summary.totalDurationMs));
|
|
108
|
+
}
|
|
109
|
+
subtitleParts.push(`${iterationsTotal} iter`);
|
|
110
|
+
if (tasksCompleted !== null && tasksTotal !== null) {
|
|
111
|
+
subtitleParts.push(`${tasksCompleted}/${tasksTotal} tasks`);
|
|
112
|
+
}
|
|
113
|
+
if (totalTokens > 0) {
|
|
114
|
+
subtitleParts.push(`${formatNumber(totalTokens)} tokens`);
|
|
115
|
+
}
|
|
116
|
+
return (_jsxs(SummaryBox, { minWidth: 60, children: [_jsx(Text, { bold: true, children: summary.feature }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsxs(Text, { bold: true, color: exitStatus.color, children: [statusIcon, " ", exitStatus.label] }), _jsx(Text, { dimColor: true, children: "\u00B7" }), _jsx(Text, { dimColor: true, children: subtitleParts.join(' · ') })] }), _jsxs(SummaryBoxSection, { children: [_jsx(Text, { bold: true, children: "Phases" }), summary.phases && summary.phases.length > 0 ? (summary.phases.map((phaseInfo) => {
|
|
117
|
+
const phaseIcon = phaseInfo.status === 'success' ? phase.complete :
|
|
53
118
|
phaseInfo.status === 'failed' ? phase.error :
|
|
54
119
|
phase.pending;
|
|
55
|
-
const
|
|
120
|
+
const phaseColor = phaseInfo.status === 'success' ? colors.green :
|
|
56
121
|
phaseInfo.status === 'failed' ? colors.pink :
|
|
57
122
|
colors.gray;
|
|
58
123
|
const durationText = phaseInfo.durationMs !== undefined
|
|
@@ -63,6 +128,31 @@ export function RunCompletionSummary({ summary, }) {
|
|
|
63
128
|
: '';
|
|
64
129
|
const statusText = phaseInfo.status === 'skipped' ? ' skipped' :
|
|
65
130
|
phaseInfo.status === 'failed' ? ' failed' : '';
|
|
66
|
-
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color:
|
|
67
|
-
})) : (_jsx(Text, { children: "No phase information available" }))] }), _jsxs(SummaryBoxSection, { children: [_jsx(Text, { bold: true, children: "Changes" }), summary.changes
|
|
131
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: phaseColor, children: [phaseIcon, " "] }), _jsxs(Text, { children: [phaseInfo.label, " ", durationText, iterationsText, statusText] })] }, phaseInfo.id));
|
|
132
|
+
})) : (_jsx(Text, { children: "No phase information available" }))] }), _jsxs(SummaryBoxSection, { children: [_jsx(Text, { bold: true, children: "Changes" }), !summary.changes || !summary.changes.available ? (_jsx(Text, { children: "Changes: Not available" })) : summary.changes.totalFilesChanged === 0 || (summary.changes.files && summary.changes.files.length === 0) ? (_jsx(Text, { children: "No changes" })) : summary.changes.totalFilesChanged !== undefined ? (_jsxs(Text, { children: [summary.changes.totalFilesChanged, " file", summary.changes.totalFilesChanged !== 1 ? 's' : '', " changed"] })) : !summary.changes.files ? (_jsx(Text, { children: "Changes: Could not compute diff" })) : null, (() => {
|
|
133
|
+
const files = summary.changes?.available ? summary.changes.files : undefined;
|
|
134
|
+
if (!files || files.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
return formatChangesFiles(files, contentWidth).map((fmt, i) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { children: fmt.displayPath }), _jsx(Text, { children: ' ' }), _jsx(Text, { color: colors.green, children: fmt.addedStr }), _jsx(Text, { children: " " }), _jsx(Text, { color: colors.pink, children: fmt.removedStr })] }, files[i].path)));
|
|
137
|
+
})(), (() => {
|
|
138
|
+
if (!summary.commits || !summary.commits.available) {
|
|
139
|
+
return _jsx(Text, { children: "Commit: Not available" });
|
|
140
|
+
}
|
|
141
|
+
if (summary.commits.commitList && summary.commits.commitList.length > 0) {
|
|
142
|
+
// hash (7) + space (1) = 8 chars reserved for prefix
|
|
143
|
+
const titleMaxWidth = contentWidth - 8;
|
|
144
|
+
return [
|
|
145
|
+
_jsx(Text, { children: ' ' }, "commit-spacer"),
|
|
146
|
+
_jsx(Text, { bold: true, children: "Commits" }, "commit-label"),
|
|
147
|
+
...summary.commits.commitList.map((commit) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { dimColor: true, children: commit.hash }), _jsxs(Text, { children: [" ", truncateEnd(commit.title, titleMaxWidth)] })] }, commit.hash))),
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
if (summary.commits.fromHash && summary.commits.toHash) {
|
|
151
|
+
return (_jsxs(Text, { children: ["Commit: ", summary.commits.fromHash, " \u2192 ", summary.commits.toHash, summary.commits.mergeType === 'squash' && ' (squash-merged)', summary.commits.mergeType === 'normal' && ' (merged)'] }));
|
|
152
|
+
}
|
|
153
|
+
if (summary.commits.toHash) {
|
|
154
|
+
return _jsxs(Text, { children: ["Commit: ", summary.commits.toHash] });
|
|
155
|
+
}
|
|
156
|
+
return _jsx(Text, { children: "Commit: Not available" });
|
|
157
|
+
})()] }), _jsxs(SummaryBoxSection, { children: [summary.pr ? (_jsx(_Fragment, { children: !summary.pr.available ? (_jsx(Text, { children: "PR: Not available" })) : summary.pr.created && summary.pr.number && summary.pr.url ? (_jsxs(Text, { children: ["PR #", summary.pr.number, ": ", summary.pr.url] })) : (_jsx(Text, { children: "PR: Not created" })) })) : (_jsx(Text, { children: "PR: Not available" })), summary.issue ? (_jsx(_Fragment, { children: !summary.issue.available ? (_jsx(Text, { children: "Issue: Not available" })) : summary.issue.linked && summary.issue.number ? (_jsxs(Text, { children: ["Issue #", summary.issue.number, ": ", summary.issue.status || 'Linked', summary.issue.url && ` (${summary.issue.url})`] })) : (_jsx(Text, { children: "Issue: Not linked" })) })) : (_jsx(Text, { children: "Issue: Not available" }))] })] }));
|
|
68
158
|
}
|