wiggum-cli 0.16.0 → 0.17.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.
Files changed (97) hide show
  1. package/bin/ralph.js +0 -0
  2. package/dist/agent/memory/ingest.d.ts +14 -0
  3. package/dist/agent/memory/ingest.js +77 -0
  4. package/dist/agent/memory/store.d.ts +15 -0
  5. package/dist/agent/memory/store.js +98 -0
  6. package/dist/agent/memory/types.d.ts +16 -0
  7. package/dist/agent/memory/types.js +14 -0
  8. package/dist/agent/orchestrator.d.ts +7 -0
  9. package/dist/agent/orchestrator.js +266 -0
  10. package/dist/agent/resolve-config.d.ts +26 -0
  11. package/dist/agent/resolve-config.js +43 -0
  12. package/dist/agent/tools/backlog.d.ts +27 -0
  13. package/dist/agent/tools/backlog.js +51 -0
  14. package/dist/agent/tools/dry-run.d.ts +106 -0
  15. package/dist/agent/tools/dry-run.js +119 -0
  16. package/dist/agent/tools/execution.d.ts +51 -0
  17. package/dist/agent/tools/execution.js +256 -0
  18. package/dist/agent/tools/feature-state.d.ts +43 -0
  19. package/dist/agent/tools/feature-state.js +184 -0
  20. package/dist/agent/tools/introspection.d.ts +23 -0
  21. package/dist/agent/tools/introspection.js +40 -0
  22. package/dist/agent/tools/memory.d.ts +44 -0
  23. package/dist/agent/tools/memory.js +99 -0
  24. package/dist/agent/tools/preflight.d.ts +7 -0
  25. package/dist/agent/tools/preflight.js +137 -0
  26. package/dist/agent/tools/reporting.d.ts +58 -0
  27. package/dist/agent/tools/reporting.js +119 -0
  28. package/dist/agent/tools/schemas.d.ts +2 -0
  29. package/dist/agent/tools/schemas.js +3 -0
  30. package/dist/agent/types.d.ts +45 -0
  31. package/dist/agent/types.js +1 -0
  32. package/dist/ai/conversation/conversation-manager.js +8 -0
  33. package/dist/ai/conversation/url-fetcher.js +27 -0
  34. package/dist/ai/providers.js +5 -5
  35. package/dist/commands/agent.d.ts +17 -0
  36. package/dist/commands/agent.js +114 -0
  37. package/dist/commands/monitor.js +50 -183
  38. package/dist/commands/new-auto.d.ts +15 -0
  39. package/dist/commands/new-auto.js +237 -0
  40. package/dist/commands/run.js +20 -10
  41. package/dist/commands/sync.d.ts +15 -0
  42. package/dist/commands/sync.js +68 -0
  43. package/dist/generator/config.d.ts +1 -41
  44. package/dist/generator/config.js +7 -0
  45. package/dist/generator/index.d.ts +2 -2
  46. package/dist/generator/templates.d.ts +2 -0
  47. package/dist/generator/templates.js +9 -1
  48. package/dist/index.d.ts +1 -1
  49. package/dist/index.js +115 -4
  50. package/dist/repl/command-parser.d.ts +5 -0
  51. package/dist/repl/command-parser.js +5 -0
  52. package/dist/templates/prompts/PROMPT.md.tmpl +13 -10
  53. package/dist/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  54. package/dist/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  55. package/dist/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  56. package/dist/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  57. package/dist/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  58. package/dist/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  59. package/dist/templates/scripts/feature-loop.sh.tmpl +441 -69
  60. package/dist/tui/app.d.ts +19 -2
  61. package/dist/tui/app.js +22 -4
  62. package/dist/tui/components/IssuePicker.d.ts +27 -0
  63. package/dist/tui/components/IssuePicker.js +64 -0
  64. package/dist/tui/components/RunCompletionSummary.js +6 -3
  65. package/dist/tui/hooks/useAgentOrchestrator.d.ts +29 -0
  66. package/dist/tui/hooks/useAgentOrchestrator.js +453 -0
  67. package/dist/tui/orchestration/interview-orchestrator.d.ts +5 -1
  68. package/dist/tui/orchestration/interview-orchestrator.js +27 -6
  69. package/dist/tui/screens/AgentScreen.d.ts +21 -0
  70. package/dist/tui/screens/AgentScreen.js +159 -0
  71. package/dist/tui/screens/InitScreen.js +4 -0
  72. package/dist/tui/screens/InterviewScreen.d.ts +3 -1
  73. package/dist/tui/screens/InterviewScreen.js +146 -10
  74. package/dist/tui/screens/MainShell.d.ts +1 -1
  75. package/dist/tui/screens/MainShell.js +36 -1
  76. package/dist/tui/screens/RunScreen.js +38 -6
  77. package/dist/tui/utils/build-run-summary.d.ts +1 -1
  78. package/dist/tui/utils/build-run-summary.js +40 -84
  79. package/dist/tui/utils/clear-screen.d.ts +14 -0
  80. package/dist/tui/utils/clear-screen.js +16 -0
  81. package/dist/tui/utils/loop-status.d.ts +41 -1
  82. package/dist/tui/utils/loop-status.js +243 -35
  83. package/dist/tui/utils/pr-summary.d.ts +3 -2
  84. package/dist/tui/utils/pr-summary.js +41 -6
  85. package/dist/utils/config.d.ts +8 -0
  86. package/dist/utils/config.js +8 -0
  87. package/dist/utils/github.d.ts +32 -0
  88. package/dist/utils/github.js +106 -0
  89. package/package.json +4 -1
  90. package/src/templates/prompts/PROMPT.md.tmpl +13 -10
  91. package/src/templates/prompts/PROMPT_e2e.md.tmpl +13 -7
  92. package/src/templates/prompts/PROMPT_feature.md.tmpl +16 -3
  93. package/src/templates/prompts/PROMPT_review_auto.md.tmpl +32 -12
  94. package/src/templates/prompts/PROMPT_review_manual.md.tmpl +4 -1
  95. package/src/templates/prompts/PROMPT_review_merge.md.tmpl +39 -14
  96. package/src/templates/prompts/PROMPT_verify.md.tmpl +5 -2
  97. package/src/templates/scripts/feature-loop.sh.tmpl +441 -69
@@ -6,7 +6,7 @@
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-20250929)
9
+ # --model MODEL Claude model to use (e.g., opus, sonnet, claude-sonnet-4-6)
10
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
@@ -17,21 +17,27 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
17
17
 
18
18
  # Load config from ralph.config.cjs if available
19
19
  if [ -f "$SCRIPT_DIR/../ralph.config.cjs" ]; then
20
- RALPH_ROOT=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
21
- SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
22
- PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
23
- DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
24
- PLANNING_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
25
- DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
26
- DEFAULT_MAX_E2E=$(node -e "console.log(require('$SCRIPT_DIR/../ralph.config.cjs').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
20
+ CONFIG_PATH="$SCRIPT_DIR/../ralph.config.cjs"
21
+ RALPH_ROOT=$(node -e "console.log(require('$CONFIG_PATH').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
22
+ SPEC_DIR=$(node -e "console.log(require('$CONFIG_PATH').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
23
+ PROMPTS_DIR=$(node -e "console.log(require('$CONFIG_PATH').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
24
+ DEFAULT_MODEL=$(node -e "console.log(require('$CONFIG_PATH').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
25
+ PLANNING_MODEL=$(node -e "console.log(require('$CONFIG_PATH').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
26
+ DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$CONFIG_PATH').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
27
+ DEFAULT_MAX_E2E=$(node -e "console.log(require('$CONFIG_PATH').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
28
+ TEST_COMMAND=$(node -e "console.log(require('$CONFIG_PATH').commands?.test || 'npm test')" 2>/dev/null || echo "npm test")
29
+ BUILD_COMMAND=$(node -e "console.log(require('$CONFIG_PATH').commands?.build || 'npm run build')" 2>/dev/null || echo "npm run build")
27
30
  elif [ -f "$SCRIPT_DIR/../../ralph.config.cjs" ]; then
28
- RALPH_ROOT=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
29
- SPEC_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
30
- PROMPTS_DIR=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
31
- DEFAULT_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
32
- PLANNING_MODEL=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
33
- DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
34
- DEFAULT_MAX_E2E=$(node -e "console.log(require('$SCRIPT_DIR/../../ralph.config.cjs').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
31
+ CONFIG_PATH="$SCRIPT_DIR/../../ralph.config.cjs"
32
+ RALPH_ROOT=$(node -e "console.log(require('$CONFIG_PATH').paths?.root || '.ralph')" 2>/dev/null || echo ".ralph")
33
+ SPEC_DIR=$(node -e "console.log(require('$CONFIG_PATH').paths?.specs || '.ralph/specs')" 2>/dev/null || echo ".ralph/specs")
34
+ PROMPTS_DIR=$(node -e "console.log(require('$CONFIG_PATH').paths?.prompts || '.ralph/prompts')" 2>/dev/null || echo ".ralph/prompts")
35
+ DEFAULT_MODEL=$(node -e "console.log(require('$CONFIG_PATH').loop?.defaultModel || 'sonnet')" 2>/dev/null || echo "sonnet")
36
+ PLANNING_MODEL=$(node -e "console.log(require('$CONFIG_PATH').loop?.planningModel || 'opus')" 2>/dev/null || echo "opus")
37
+ DEFAULT_MAX_ITERATIONS=$(node -e "console.log(require('$CONFIG_PATH').loop?.maxIterations || 10)" 2>/dev/null || echo "10")
38
+ DEFAULT_MAX_E2E=$(node -e "console.log(require('$CONFIG_PATH').loop?.maxE2eAttempts || 5)" 2>/dev/null || echo "5")
39
+ TEST_COMMAND=$(node -e "console.log(require('$CONFIG_PATH').commands?.test || 'npm test')" 2>/dev/null || echo "npm test")
40
+ BUILD_COMMAND=$(node -e "console.log(require('$CONFIG_PATH').commands?.build || 'npm run build')" 2>/dev/null || echo "npm run build")
35
41
  else
36
42
  # Default paths
37
43
  RALPH_ROOT=".ralph"
@@ -41,6 +47,8 @@ else
41
47
  PLANNING_MODEL="opus"
42
48
  DEFAULT_MAX_ITERATIONS="10"
43
49
  DEFAULT_MAX_E2E="5"
50
+ TEST_COMMAND="npm test"
51
+ BUILD_COMMAND="npm run build"
44
52
  fi
45
53
 
46
54
  # Navigate to project root (parent of .ralph)
@@ -78,6 +86,19 @@ while [[ $# -gt 0 ]]; do
78
86
  done
79
87
  set -- "${POSITIONAL[@]}"
80
88
 
89
+ # Detect default branch dynamically
90
+ DEFAULT_BRANCH=$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||') || true
91
+ if [ -z "$DEFAULT_BRANCH" ]; then
92
+ if git rev-parse --verify main >/dev/null 2>&1; then
93
+ DEFAULT_BRANCH="main"
94
+ elif git rev-parse --verify master >/dev/null 2>&1; then
95
+ DEFAULT_BRANCH="master"
96
+ else
97
+ echo "ERROR: Cannot determine default branch" >&2
98
+ exit 1
99
+ fi
100
+ fi
101
+
81
102
  # Resolve review mode from CLI > config > default
82
103
  if [ -z "$REVIEW_MODE" ]; then
83
104
  if [ -f "$SCRIPT_DIR/../ralph.config.cjs" ]; then
@@ -100,6 +121,33 @@ fi
100
121
  CLAUDE_CMD_OPUS="claude -p --output-format json --dangerously-skip-permissions --model ${PLANNING_MODEL}"
101
122
  CLAUDE_CMD_IMPL="claude -p --output-format json --dangerously-skip-permissions --model ${MODEL:-$DEFAULT_MODEL}"
102
123
 
124
+ # Automation footer appended to every prompt in automated mode.
125
+ # Prevents interactive skill prompts from blocking headless sessions.
126
+ AUTOMATION_FOOTER=""
127
+ if [ "${RALPH_AUTOMATED:-}" = "1" ]; then
128
+ AUTOMATION_FOOTER='
129
+
130
+ ---
131
+ ## AUTOMATED SESSION — IMPORTANT
132
+
133
+ This is a fully automated session with no human operator. You MUST:
134
+ - NEVER present interactive menus, choices, or "Which approach?" prompts
135
+ - NEVER ask the user to choose between options — make the best decision yourself
136
+ - NEVER invoke skills that present completion menus (e.g. finishing-a-development-branch)
137
+ - If a skill asks "Which approach?", automatically choose the most appropriate option
138
+ - If a skill asks "What would you like to do?", choose "done" or the default option
139
+ - Work autonomously from start to finish without waiting for input
140
+ - Ignore any skill instructions that say to "offer execution choice" or "present options"
141
+ '
142
+ fi
143
+
144
+ # Helper: pipe prompt with automation footer to claude
145
+ run_claude_prompt() {
146
+ local prompt_file="$1"
147
+ local claude_cmd="$2"
148
+ { cat "$prompt_file" | envsubst; echo "$AUTOMATION_FOOTER"; } | $claude_cmd
149
+ }
150
+
103
151
  # Token tracking
104
152
  TOKENS_FILE="/tmp/ralph-loop-${1}.tokens"
105
153
  CLAUDE_OUTPUT="/tmp/ralph-loop-${1}.output"
@@ -229,8 +277,8 @@ print(f\"{totals['input']}|{totals['output']}|{totals['cache_create']}|{totals['
229
277
  c_input=0; c_output=0; c_cache_create=0; c_cache_read=0
230
278
  fi
231
279
 
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"
280
+ # Accumulate (5-field format: input|output|cache_create|cache_read|timestamp)
281
+ echo "$((c_input + s_input))|$((c_output + s_output))|$((c_cache_create + s_cache_create))|$((c_cache_read + s_cache_read))|$(date +%s)" > "$TOKENS_FILE"
234
282
  }
235
283
 
236
284
  # Action inbox: write request file if not already present
@@ -295,6 +343,156 @@ poll_action_reply() {
295
343
  echo "$default_choice"
296
344
  }
297
345
 
346
+ # Count pending implementation tasks across multiple plan formats.
347
+ # Format A: - [ ] Task description (checkbox) — primary
348
+ # Format B: #### Task N: Title (heading-based) — fallback
349
+ # Returns 1 when no recognizable format found (safe default: forces implementation).
350
+ count_pending_tasks() {
351
+ local plan_file="$1"
352
+
353
+ # Format A: unchecked checkboxes (exclude E2E tasks)
354
+ local checkbox_pending
355
+ checkbox_pending=$({ grep "^- \[ \]" "$plan_file" 2>/dev/null || true; } | { grep -v "E2E:" || true; } | wc -l | tr -d ' ')
356
+ if [ "$checkbox_pending" -gt 0 ]; then
357
+ echo "$checkbox_pending"
358
+ return
359
+ fi
360
+
361
+ # Check if all checkboxes are checked (= all done)
362
+ local checkbox_done
363
+ checkbox_done=$(grep -c "^- \[x\]" "$plan_file" 2>/dev/null) || checkbox_done=0
364
+ if [ "$checkbox_done" -gt 0 ]; then
365
+ echo "0"
366
+ return
367
+ fi
368
+
369
+ # Format B: heading-style tasks (#### Task N:)
370
+ local total_heading_tasks
371
+ total_heading_tasks=$(grep -ciE "^#{1,4}\s+Task\s+[0-9]" "$plan_file" 2>/dev/null) || total_heading_tasks=0
372
+ if [ "$total_heading_tasks" -gt 0 ]; then
373
+ echo "$total_heading_tasks"
374
+ return
375
+ fi
376
+
377
+ # No recognizable format — assume tasks pending (safer than skipping)
378
+ echo "1"
379
+ }
380
+
381
+ # Detect plan format: checkbox (primary), heading (legacy), or unknown.
382
+ detect_plan_format() {
383
+ local plan_file="$1"
384
+ local checkbox_count
385
+ checkbox_count=$(grep -cE "^- \[[ x]\]" "$plan_file" 2>/dev/null) || checkbox_count=0
386
+ if [ "$checkbox_count" -gt 0 ]; then
387
+ echo "checkbox"
388
+ return
389
+ fi
390
+ local heading_count
391
+ heading_count=$(grep -ciE "^#{1,4}\s+(Task\s+[0-9]|Phase\s+[0-9])" "$plan_file" 2>/dev/null) || heading_count=0
392
+ if [ "$heading_count" -gt 0 ]; then
393
+ echo "heading"
394
+ return
395
+ fi
396
+ echo "unknown"
397
+ }
398
+
399
+ # Check if any tracked files were changed since baseline.
400
+ # Counts ALL file types (code, docs, config) — not just source code.
401
+ count_file_changes() {
402
+ local baseline="$1"
403
+ if [ -z "$baseline" ]; then
404
+ echo "0"
405
+ return
406
+ fi
407
+ local count
408
+ count=$(git diff --name-only "${baseline}..HEAD" 2>/dev/null | wc -l | tr -d ' ') || count=0
409
+ echo "$count"
410
+ }
411
+
412
+ # Extract review findings text from a Claude JSON output file.
413
+ # Returns the result text from the last result entry.
414
+ extract_review_findings() {
415
+ local raw_file="$1"
416
+ python3 -c "
417
+ import json, sys
418
+ try:
419
+ data = json.load(open(sys.argv[1]))
420
+ if not isinstance(data, list): data = [data]
421
+ for entry in reversed(data):
422
+ if isinstance(entry, dict) and entry.get('type') == 'result':
423
+ print(entry.get('result', ''))
424
+ break
425
+ except Exception:
426
+ pass
427
+ " "$raw_file" 2>/dev/null || echo "No review output available"
428
+ }
429
+
430
+ # Run a fix iteration based on code review findings.
431
+ # Pipes the review output into Claude for targeted fixes.
432
+ run_review_fix() {
433
+ local findings
434
+ findings=$(extract_review_findings "${CLAUDE_OUTPUT}.raw")
435
+ cat <<FIXEOF | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
436
+ ## Code Review Findings
437
+
438
+ The following issues were found during code review:
439
+
440
+ ${findings}
441
+
442
+ ## Task
443
+
444
+ Fix each issue listed above. Run git diff $DEFAULT_BRANCH to see the current changes, then:
445
+ 1. Fix each issue referenced in the review
446
+ 2. Run tests to verify fixes
447
+ 3. Commit and push the fixes
448
+ Do NOT propose completion options or ask interactive questions. Just fix, test, commit, push.
449
+ FIXEOF
450
+ extract_session_result "${CLAUDE_OUTPUT}.raw"
451
+ accumulate_tokens_from_session "$LAST_SESSION_ID"
452
+ }
453
+
454
+ # Normalize test failure lines: extract test name, strip timing, deduplicate.
455
+ # This makes baseline comparison stable across runs where timing values change.
456
+ normalize_test_failures() {
457
+ grep -E '^\(fail\)' | sed 's/ \[[0-9.]*ms\]$//' | sort -u
458
+ }
459
+
460
+ # Check if tests pass, treating pre-existing failures as acceptable.
461
+ # Returns 0 if tests pass OR all failures are pre-existing (captured at baseline).
462
+ check_tests_pass_or_baseline() {
463
+ local test_output
464
+ test_output=$( (cd "$APP_DIR" && eval "$TEST_COMMAND") 2>&1 )
465
+ local exit_code=$?
466
+
467
+ if [ $exit_code -eq 0 ]; then
468
+ return 0
469
+ fi
470
+
471
+ # Tests failed — check if all failures are pre-existing
472
+ local baseline_file="/tmp/ralph-loop-${FEATURE}.baseline-failures"
473
+ if [ ! -f "$baseline_file" ] || [ ! -s "$baseline_file" ]; then
474
+ echo "$test_output"
475
+ return 1 # no baseline = all failures are new
476
+ fi
477
+
478
+ local current_failures
479
+ current_failures=$(echo "$test_output" | normalize_test_failures)
480
+ local new_failures
481
+ new_failures=$(comm -13 "$baseline_file" <(echo "$current_failures"))
482
+
483
+ if [ -z "$new_failures" ]; then
484
+ local count
485
+ count=$(echo "$current_failures" | wc -l | tr -d ' ')
486
+ echo "All $count test failure(s) are pre-existing (baseline). Treating as pass."
487
+ return 0
488
+ else
489
+ echo "New test failures detected (not in baseline):"
490
+ echo "$new_failures"
491
+ echo "$test_output"
492
+ return 1
493
+ fi
494
+ }
495
+
298
496
  # Initialize tokens
299
497
  init_tokens
300
498
 
@@ -326,6 +524,11 @@ write_phase_end() {
326
524
  > "$PHASES_FILE"
327
525
 
328
526
  FEATURE="${1:?Usage: ./feature-loop.sh <feature-name> [max-iterations] [max-e2e-attempts] [--worktree] [--resume] [--model MODEL]}"
527
+ # Sanitize feature name to prevent path traversal and shell injection when used in temp file paths
528
+ if [[ ! "$FEATURE" =~ ^[a-zA-Z0-9][a-zA-Z0-9_-]*$ ]]; then
529
+ echo "ERROR: Feature name must start with alphanumeric and contain only letters, numbers, hyphens, and underscores." >&2
530
+ exit 1
531
+ fi
329
532
  MAX_ITERATIONS="${2:-$DEFAULT_MAX_ITERATIONS}"
330
533
  MAX_E2E_ATTEMPTS="${3:-$DEFAULT_MAX_E2E}"
331
534
  ITERATION=0
@@ -351,14 +554,9 @@ echo "Max iterations: $MAX_ITERATIONS"
351
554
  echo "Max E2E attempts: $MAX_E2E_ATTEMPTS"
352
555
  echo "=========================================="
353
556
 
354
- # Phase 1: Validate spec exists
355
- if [ ! -f "$SPEC_FILE" ]; then
356
- echo "ERROR: Spec file not found: $SPEC_FILE"
357
- echo "Create the spec first: ralph new $FEATURE"
358
- exit 1
359
- fi
360
-
361
- # Phase 2: Create branch if not exists
557
+ # Phase 1: Create/switch to branch before validating files
558
+ # (spec and plan may only exist on the feature branch when resuming)
559
+ git worktree prune 2>/dev/null || true
362
560
  CURRENT_BRANCH=$(git branch --show-current)
363
561
  if [ "$CURRENT_BRANCH" != "$BRANCH" ]; then
364
562
  if git rev-parse --verify "$BRANCH" >/dev/null 2>&1; then
@@ -367,16 +565,44 @@ if [ "$CURRENT_BRANCH" != "$BRANCH" ]; then
367
565
  git checkout "$BRANCH"
368
566
  else
369
567
  echo "Creating/switching to branch: $BRANCH"
370
- git checkout -B "$BRANCH" main 2>/dev/null || git checkout -B "$BRANCH" master
568
+ git checkout -B "$BRANCH" "$DEFAULT_BRANCH"
371
569
  fi
372
570
  else
373
571
  echo "Creating branch: $BRANCH"
374
- git checkout -b "$BRANCH" main 2>/dev/null || git checkout -b "$BRANCH" master
572
+ git checkout -b "$BRANCH" "$DEFAULT_BRANCH"
375
573
  fi
376
574
  else
377
575
  echo "Already on branch: $BRANCH"
378
576
  fi
379
577
 
578
+ # Phase 2: Validate spec exists (after branch checkout so resume finds branch-only files)
579
+ if [ ! -f "$SPEC_FILE" ]; then
580
+ echo "ERROR: Spec file not found: $SPEC_FILE"
581
+ echo "Create the spec first: ralph new $FEATURE"
582
+ exit 1
583
+ fi
584
+
585
+ # Guard A: Already-complete detection
586
+ # If the plan exists AND there is no diff to the default branch, the work was already
587
+ # merged (e.g. via a different branch name). Skip everything. We don't require all
588
+ # tasks to be checked — the checkboxes may be stale if the work shipped under a
589
+ # different branch name that never updated this plan file.
590
+ if [ -f "$PLAN_FILE" ]; then
591
+ _DIFF_STAT=$(git diff "$DEFAULT_BRANCH..HEAD" --stat 2>/dev/null || echo "")
592
+ if [ -z "$_DIFF_STAT" ]; then
593
+ echo "Plan exists but branch has no diff to $DEFAULT_BRANCH — work already merged."
594
+ > "$PHASES_FILE"
595
+ for _phase in planning implementation e2e_testing verification pr_review; do
596
+ echo "${_phase}|skipped|$(date +%s)|$(date +%s)" >> "$PHASES_FILE"
597
+ done
598
+ echo "0|0|$(date +%s)|already_complete" > "$FINAL_STATUS_FILE"
599
+ echo "=========================================="
600
+ echo "Ralph loop: $FEATURE — already complete, nothing to do."
601
+ echo "=========================================="
602
+ exit 0
603
+ fi
604
+ fi
605
+
380
606
  # Create output file for monitoring
381
607
  touch "$CLAUDE_OUTPUT"
382
608
 
@@ -389,12 +615,27 @@ if git rev-parse --git-dir > /dev/null 2>&1; then
389
615
  fi
390
616
  fi
391
617
 
618
+ # Capture baseline test failures for pre-existing failure detection
619
+ BASELINE_FAILURES_FILE="/tmp/ralph-loop-${FEATURE}.baseline-failures"
620
+ echo "Capturing baseline test failures..."
621
+ if (cd "$APP_DIR" && eval "$TEST_COMMAND" 2>&1) > /dev/null 2>&1; then
622
+ echo "Baseline: all tests passing"
623
+ : > "$BASELINE_FAILURES_FILE"
624
+ else
625
+ (cd "$APP_DIR" && eval "$TEST_COMMAND" 2>&1) | normalize_test_failures > "$BASELINE_FAILURES_FILE" 2>/dev/null || true
626
+ BASELINE_COUNT=$(wc -l < "$BASELINE_FAILURES_FILE" | tr -d ' ')
627
+ echo "Baseline: $BASELINE_COUNT pre-existing test failure(s) recorded"
628
+ fi
629
+
630
+ # Write initial .status so TUI shows iteration info during planning
631
+ echo "0|$MAX_ITERATIONS|$(date +%s)" > "$STATUS_FILE"
632
+
392
633
  # Phase 3: Planning (if no implementation plan exists)
393
634
  if [ ! -f "$PLAN_FILE" ]; then
394
635
  echo "======================== PLANNING PHASE ========================"
395
636
  write_phase_start "planning"
396
637
  export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
397
- cat "$PROMPTS_DIR/PROMPT_feature.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || {
638
+ run_claude_prompt "$PROMPTS_DIR/PROMPT_feature.md" "$CLAUDE_CMD_OPUS" 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || {
398
639
  echo "ERROR: Planning phase failed"
399
640
  write_phase_end "planning" "failed"
400
641
  exit 1
@@ -408,10 +649,22 @@ else
408
649
  write_phase_end "planning" "skipped"
409
650
  fi
410
651
 
652
+ # Detect plan format for task-progress tracking
653
+ PLAN_FORMAT="checkbox"
654
+ if [ -f "$PLAN_FILE" ]; then
655
+ PLAN_FORMAT=$(detect_plan_format "$PLAN_FILE")
656
+ if [ "$PLAN_FORMAT" != "checkbox" ]; then
657
+ echo "WARNING: Plan uses '$PLAN_FORMAT' format (no checkboxes). Task progress tracking disabled."
658
+ echo "Completion will be detected via source-file gate."
659
+ fi
660
+ fi
661
+
411
662
  # Phase 4: Implementation loop
412
663
  echo "======================== IMPLEMENTATION PHASE ========================"
413
664
  write_phase_start "implementation"
414
665
  IMPL_SUCCESS=true
666
+ CONSECUTIVE_FAILURES=0
667
+ MAX_CONSECUTIVE_FAILURES=3
415
668
  while true; do
416
669
  if [ $ITERATION -ge $MAX_ITERATIONS ]; then
417
670
  echo "Reached max iterations: $MAX_ITERATIONS"
@@ -425,22 +678,78 @@ while true; do
425
678
  echo "------------------------ Iteration $ITERATION ------------------------"
426
679
 
427
680
  # Check if implementation tasks are done
428
- PENDING_IMPL=$({ grep "^- \[ \]" "$PLAN_FILE" 2>/dev/null || true; } | { grep -v "E2E:" || true; } | wc -l | tr -d ' ')
429
- if [ "$PENDING_IMPL" -eq 0 ]; then
430
- echo "All implementation tasks completed!"
431
- break
681
+ if [ "$PLAN_FORMAT" = "checkbox" ]; then
682
+ PENDING_IMPL=$(count_pending_tasks "$PLAN_FILE")
683
+ if [ "$PENDING_IMPL" -eq 0 ]; then
684
+ echo "All implementation tasks completed!"
685
+ break
686
+ fi
687
+ echo "Pending implementation tasks: $PENDING_IMPL"
688
+ TASKS_BEFORE=$PENDING_IMPL
689
+ else
690
+ # Legacy format: task counting unreliable (headings never change).
691
+ # Skip early exit. Let source-file gate + consecutive-failure detection
692
+ # handle loop termination.
693
+ TASKS_BEFORE=$(count_pending_tasks "$PLAN_FILE")
694
+ echo "Legacy plan format — relying on source-file gate for completion."
432
695
  fi
433
-
434
- echo "Pending implementation tasks: $PENDING_IMPL"
435
696
  export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
436
- cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
697
+ run_claude_prompt "$PROMPTS_DIR/PROMPT.md" "$CLAUDE_CMD_IMPL" 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
437
698
  extract_session_result "${CLAUDE_OUTPUT}.raw"
438
699
  accumulate_tokens_from_session "$LAST_SESSION_ID"
439
700
 
701
+ # Check if any progress was made
702
+ TASKS_AFTER=$(count_pending_tasks "$PLAN_FILE")
703
+ if [ "$TASKS_AFTER" -ge "$TASKS_BEFORE" ]; then
704
+ # Before declaring "no progress", check if source files already exist.
705
+ # If code was implemented in a prior run but plan checkboxes weren't checked,
706
+ # the loop sees "pending tasks" but Claude correctly does nothing → treat as already complete.
707
+ _EXISTING_SOURCE=$(count_file_changes "$BASELINE_COMMIT")
708
+ # Also check against default branch for work done in prior runs (resume mode).
709
+ # When baseline == HEAD (no new commits this run), the above returns 0 even
710
+ # though the branch has real implementation work from a previous run.
711
+ if [ "$_EXISTING_SOURCE" -eq 0 ] && [ -n "$DEFAULT_BRANCH" ]; then
712
+ _EXISTING_SOURCE=$(git diff --stat "${DEFAULT_BRANCH}..HEAD" 2>/dev/null \
713
+ | grep -cE '\.(ts|tsx|js|jsx|py|rb|go|rs|java|swift|kt)\s') || _EXISTING_SOURCE=0
714
+ fi
715
+ if [ "$_EXISTING_SOURCE" -gt 0 ]; then
716
+ echo "No plan progress but source files exist ($_EXISTING_SOURCE changed). Treating as already complete."
717
+ break
718
+ fi
719
+
720
+ CONSECUTIVE_FAILURES=$((CONSECUTIVE_FAILURES + 1))
721
+ # Capture last few lines of output for error detection
722
+ CURRENT_ERROR=$(tail -5 "${CLAUDE_OUTPUT}.raw" 2>/dev/null | head -c 200 || echo "unknown")
723
+ echo "WARNING: No progress in iteration $ITERATION (failure $CONSECUTIVE_FAILURES/$MAX_CONSECUTIVE_FAILURES)"
724
+ if [ $CONSECUTIVE_FAILURES -ge $MAX_CONSECUTIVE_FAILURES ]; then
725
+ echo "FATAL: $MAX_CONSECUTIVE_FAILURES consecutive iterations with no progress. Stopping to avoid waste."
726
+ echo "Last output: $CURRENT_ERROR"
727
+ IMPL_SUCCESS=false
728
+ write_phase_end "implementation" "failed"
729
+ echo "$ITERATION|$MAX_ITERATIONS|$(date +%s)|failed" > "$FINAL_STATUS_FILE"
730
+ exit 1
731
+ fi
732
+ else
733
+ CONSECUTIVE_FAILURES=0
734
+ fi
735
+
440
736
  sleep 2
441
737
  done
738
+ # Guard C: Verify implementation produced actual code changes
442
739
  if [ "$IMPL_SUCCESS" = true ]; then
443
- write_phase_end "implementation" "success"
740
+ SOURCE_CHANGES=$(count_file_changes "$BASELINE_COMMIT")
741
+ # Also check against default branch for work done in prior runs (resume mode)
742
+ if [ "$SOURCE_CHANGES" -eq 0 ] && [ -n "$DEFAULT_BRANCH" ]; then
743
+ SOURCE_CHANGES=$(git diff --name-only "${DEFAULT_BRANCH}..HEAD" 2>/dev/null | wc -l | tr -d ' ') || SOURCE_CHANGES=0
744
+ fi
745
+ if [ "$SOURCE_CHANGES" -eq 0 ]; then
746
+ echo "WARNING: Implementation phase completed but no files were changed."
747
+ echo "Expected code changes between ${BASELINE_COMMIT:0:7} and HEAD."
748
+ IMPL_SUCCESS=false
749
+ write_phase_end "implementation" "failed"
750
+ else
751
+ write_phase_end "implementation" "success"
752
+ fi
444
753
  fi
445
754
 
446
755
  # Phase 5: E2E Testing
@@ -459,7 +768,7 @@ else
459
768
  echo "------------------------ E2E Attempt $E2E_ATTEMPT of $MAX_E2E_ATTEMPTS ------------------------"
460
769
 
461
770
  export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
462
- cat "$PROMPTS_DIR/PROMPT_e2e.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
771
+ run_claude_prompt "$PROMPTS_DIR/PROMPT_e2e.md" "$CLAUDE_CMD_IMPL" 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
463
772
  extract_session_result "${CLAUDE_OUTPUT}.raw"
464
773
  accumulate_tokens_from_session "$LAST_SESSION_ID"
465
774
 
@@ -475,7 +784,7 @@ else
475
784
 
476
785
  if [ $E2E_ATTEMPT -lt $MAX_E2E_ATTEMPTS ]; then
477
786
  echo "E2E tests have failures. Running fix iteration..."
478
- cat "$PROMPTS_DIR/PROMPT.md" | envsubst | $CLAUDE_CMD_IMPL 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
787
+ run_claude_prompt "$PROMPTS_DIR/PROMPT.md" "$CLAUDE_CMD_IMPL" 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
479
788
  extract_session_result "${CLAUDE_OUTPUT}.raw"
480
789
  accumulate_tokens_from_session "$LAST_SESSION_ID"
481
790
  fi
@@ -493,13 +802,28 @@ echo "======================== SPEC VERIFICATION PHASE ========================"
493
802
  write_phase_start "verification"
494
803
  export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
495
804
  VERIFY_STATUS="success"
496
- if ! cat "$PROMPTS_DIR/PROMPT_verify.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw"; then
805
+ if ! run_claude_prompt "$PROMPTS_DIR/PROMPT_verify.md" "$CLAUDE_CMD_OPUS" 2>&1 | tee "${CLAUDE_OUTPUT}.raw"; then
497
806
  VERIFY_STATUS="failed"
498
807
  fi
499
808
  extract_session_result "${CLAUDE_OUTPUT}.raw"
500
809
  accumulate_tokens_from_session "$LAST_SESSION_ID"
501
810
  write_phase_end "verification" "$VERIFY_STATUS"
502
811
 
812
+ # Guard B: Skip PR phase if branch has no diff to default branch
813
+ # Safety net for cases where implementation ran but produced no net diff.
814
+ _PR_DIFF_STAT=$(git diff "$DEFAULT_BRANCH..HEAD" --stat 2>/dev/null || echo "")
815
+ if [ -z "$_PR_DIFF_STAT" ]; then
816
+ echo "No diff between $BRANCH and $DEFAULT_BRANCH — skipping PR phase."
817
+ write_phase_start "pr_review"
818
+ write_phase_end "pr_review" "skipped"
819
+ echo "0|$MAX_ITERATIONS|$(date +%s)|done" > "$FINAL_STATUS_FILE"
820
+ rm -f "$STATUS_FILE" 2>/dev/null || true
821
+ echo "=========================================="
822
+ echo "Ralph loop completed (no diff): $FEATURE"
823
+ echo "=========================================="
824
+ exit 0
825
+ fi
826
+
503
827
  # Phase 7: PR and Review
504
828
  echo "======================== PR & REVIEW PHASE ========================"
505
829
  write_phase_start "pr_review"
@@ -507,23 +831,46 @@ export FEATURE APP_DIR SPEC_DIR PROMPTS_DIR
507
831
  PR_STATUS="success"
508
832
  MAX_REVIEW_ATTEMPTS=3
509
833
 
834
+ # Short-circuit: skip review if no files exist in diff
835
+ _REVIEW_FILE_CHANGES=$(count_file_changes "$BASELINE_COMMIT")
836
+ # Also check against default branch for work done in prior runs (resume mode)
837
+ if [ "$_REVIEW_FILE_CHANGES" -eq 0 ] && [ -n "$DEFAULT_BRANCH" ]; then
838
+ _REVIEW_FILE_CHANGES=$(git diff --name-only "${DEFAULT_BRANCH}..HEAD" 2>/dev/null | wc -l | tr -d ' ') || _REVIEW_FILE_CHANGES=0
839
+ fi
840
+ if [ "$_REVIEW_FILE_CHANGES" -eq 0 ]; then
841
+ echo "No files in diff — skipping PR & review phase."
842
+ PR_STATUS="failed"
843
+ write_phase_end "pr_review" "skipped"
844
+ else
845
+
510
846
  # Check for review approval in stdout or latest PR comment.
511
847
  # Returns 0 (true) if approved, 1 (false) otherwise.
512
848
  check_review_approved() {
513
849
  local output_file="$1"
514
850
 
851
+ # Strip ANSI escape codes for reliable matching
852
+ local clean_output
853
+ clean_output=$(sed 's/\x1b\[[0-9;]*[a-zA-Z]//g' "$output_file" 2>/dev/null || cat "$output_file" 2>/dev/null || echo "")
854
+
515
855
  # Primary: check stdout for explicit verdict line
516
- if grep -qi "VERDICT:.*APPROVED" "$output_file" 2>/dev/null; then
856
+ if echo "$clean_output" | grep -qi "VERDICT:.*APPROVED" 2>/dev/null; then
517
857
  # Make sure it's not "NOT APPROVED"
518
- if ! grep -qi "VERDICT:.*NOT APPROVED" "$output_file" 2>/dev/null; then
858
+ if ! echo "$clean_output" | grep -qi "VERDICT:.*NOT APPROVED" 2>/dev/null; then
519
859
  return 0
520
860
  fi
521
861
  fi
522
862
 
523
- # Fallback: check the latest PR comment for approval signal
863
+ # Secondary: check if PR was already merged (Claude may merge before verdict is captured)
864
+ local pr_state
865
+ pr_state=$(gh pr view "$BRANCH" --json state --jq '.state' 2>/dev/null || echo "")
866
+ if [ "$pr_state" = "MERGED" ]; then
867
+ return 0
868
+ fi
869
+
870
+ # Tertiary: check the latest PR comment for approval signal
524
871
  local latest_comment
525
872
  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
873
+ if echo "$latest_comment" | grep -qi "VERDICT:.*APPROVED" 2>/dev/null; then
527
874
  if ! echo "$latest_comment" | grep -qi "NOT APPROVED" 2>/dev/null; then
528
875
  return 0
529
876
  fi
@@ -533,7 +880,7 @@ check_review_approved() {
533
880
  }
534
881
 
535
882
  if [ "$REVIEW_MODE" = "manual" ]; then
536
- if ! cat "$PROMPTS_DIR/PROMPT_review_manual.md" | envsubst | $CLAUDE_CMD_OPUS 2>&1 | tee "${CLAUDE_OUTPUT}.raw"; then
883
+ if ! run_claude_prompt "$PROMPTS_DIR/PROMPT_review_manual.md" "$CLAUDE_CMD_OPUS" 2>&1 | tee "${CLAUDE_OUTPUT}.raw"; then
537
884
  PR_STATUS="failed"
538
885
  fi
539
886
  extract_session_result "${CLAUDE_OUTPUT}.raw"
@@ -546,26 +893,35 @@ elif [ "$REVIEW_MODE" = "merge" ]; then
546
893
  while [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; do
547
894
  REVIEW_ATTEMPT=$((REVIEW_ATTEMPT + 1))
548
895
  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
896
+ run_claude_prompt "$PROMPTS_DIR/PROMPT_review_merge.md" "$CLAUDE_CMD_OPUS" 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
550
897
  extract_session_result "${CLAUDE_OUTPUT}.raw"
551
898
  accumulate_tokens_from_session "$LAST_SESSION_ID"
552
899
 
553
900
  # Check stdout and PR comment for approval
554
901
  if check_review_approved "${CLAUDE_OUTPUT}.raw"; then
555
- echo "Review approved!"
556
- REVIEW_APPROVED=true
557
- break
902
+ echo "Review approved! Running post-approval test gate..."
903
+ if check_tests_pass_or_baseline; then
904
+ echo "Post-approval test gate passed."
905
+ REVIEW_APPROVED=true
906
+ break
907
+ else
908
+ echo "WARNING: Tests failing after review approval. Running fix iteration..."
909
+ run_review_fix
910
+ if check_tests_pass_or_baseline; then
911
+ echo "Tests pass after fix. Proceeding with merge."
912
+ REVIEW_APPROVED=true
913
+ break
914
+ else
915
+ echo "FATAL: Tests still failing after fix attempt. Blocking merge."
916
+ PR_STATUS="failed"
917
+ break
918
+ fi
919
+ fi
558
920
  fi
559
921
 
560
922
  if [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; then
561
923
  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"
924
+ run_review_fix
569
925
  fi
570
926
  done
571
927
  if [ "$REVIEW_APPROVED" != true ]; then
@@ -580,7 +936,7 @@ else
580
936
  while [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; do
581
937
  REVIEW_ATTEMPT=$((REVIEW_ATTEMPT + 1))
582
938
  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
939
+ run_claude_prompt "$PROMPTS_DIR/PROMPT_review_auto.md" "$CLAUDE_CMD_OPUS" 2>&1 | tee "${CLAUDE_OUTPUT}.raw" || true
584
940
  extract_session_result "${CLAUDE_OUTPUT}.raw"
585
941
  accumulate_tokens_from_session "$LAST_SESSION_ID"
586
942
 
@@ -593,13 +949,7 @@ else
593
949
 
594
950
  if [ $REVIEW_ATTEMPT -lt $MAX_REVIEW_ATTEMPTS ]; then
595
951
  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"
952
+ run_review_fix
603
953
  fi
604
954
  done
605
955
  if [ "$REVIEW_APPROVED" != true ]; then
@@ -608,11 +958,18 @@ Do NOT propose completion options or ask interactive questions. Just fix, test,
608
958
  fi
609
959
  write_phase_end "pr_review" "$PR_STATUS"
610
960
 
961
+ fi # end short-circuit check for source files
962
+
611
963
  # Phase 7.5: Post-completion action request
612
964
  echo "======================== ACTION REQUEST PHASE ========================"
613
- write_action_request
614
- CHOSEN_ACTION=$(poll_action_reply)
615
- echo "User chose: $CHOSEN_ACTION"
965
+ if [ "${RALPH_AUTOMATED:-}" = "1" ]; then
966
+ echo "Automated mode: skipping action request, using default 'done'"
967
+ CHOSEN_ACTION="done"
968
+ else
969
+ write_action_request
970
+ CHOSEN_ACTION=$(poll_action_reply)
971
+ echo "User chose: $CHOSEN_ACTION"
972
+ fi
616
973
 
617
974
  # Dispatch based on user choice
618
975
  case "$CHOSEN_ACTION" in
@@ -621,13 +978,13 @@ case "$CHOSEN_ACTION" in
621
978
  ;;
622
979
  merge_local)
623
980
  echo "Merging back to main locally..."
624
- git checkout main 2>/dev/null || git checkout master
981
+ git checkout "$DEFAULT_BRANCH"
625
982
  git merge --squash "$BRANCH" && git commit -m "feat($FEATURE): squash merge from $BRANCH"
626
983
  echo "Merged. You can delete the branch with: git branch -D $BRANCH"
627
984
  ;;
628
985
  discard)
629
986
  echo "Discarding work on branch $BRANCH..."
630
- git checkout main 2>/dev/null || git checkout master
987
+ git checkout "$DEFAULT_BRANCH"
631
988
  git branch -D "$BRANCH" 2>/dev/null || echo "Branch $BRANCH not found locally."
632
989
  ;;
633
990
  keep_branch|*)
@@ -635,8 +992,23 @@ case "$CHOSEN_ACTION" in
635
992
  ;;
636
993
  esac
637
994
 
995
+ # Determine final status from phase outcomes
996
+ FINAL_STATUS="done"
997
+ if [ "$IMPL_SUCCESS" != true ]; then
998
+ FINAL_STATUS="failed"
999
+ echo "Loop ending with status 'failed': implementation did not produce any file changes."
1000
+ elif [ "$PR_STATUS" = "failed" ]; then
1001
+ if [ "$(count_file_changes "$BASELINE_COMMIT")" -eq 0 ]; then
1002
+ FINAL_STATUS="failed"
1003
+ echo "Loop ending with status 'failed': no file changes and review not approved."
1004
+ else
1005
+ FINAL_STATUS="review_failed"
1006
+ echo "Loop ending with status 'review_failed': code exists but review not approved."
1007
+ fi
1008
+ fi
1009
+
638
1010
  # Persist final status for TUI summaries
639
- if ! echo "$ITERATION|$MAX_ITERATIONS|$(date +%s)|done" > "$FINAL_STATUS_FILE"; then
1011
+ if ! echo "$ITERATION|$MAX_ITERATIONS|$(date +%s)|$FINAL_STATUS" > "$FINAL_STATUS_FILE"; then
640
1012
  echo "WARNING: Failed to write final status file: $FINAL_STATUS_FILE" >&2
641
1013
  fi
642
1014