agentic-loop 3.10.2 → 3.11.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.
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env bash
2
+ # shellcheck shell=bash
3
+ #
4
+ # code-check.sh - Code verification pipeline for Ralph autonomous development loop
5
+ #
6
+ # ============================================================================
7
+ # OVERVIEW
8
+ # ============================================================================
9
+ # After Claude writes code for a story, this pipeline verifies the work before
10
+ # marking it complete. If verification fails, context is saved and Claude
11
+ # retries with knowledge of what went wrong.
12
+ #
13
+ # Philosophy: Claude handles complex verification (visual, UX, logic) using
14
+ # MCP browser tools. Ralph handles deterministic checks (lint, tests, commands).
15
+ #
16
+ # ============================================================================
17
+ # VERIFICATION PIPELINE (5 steps, fail-fast)
18
+ # ============================================================================
19
+ #
20
+ # [1/5] Lint checks - ESLint, Ruff, golangci-lint, etc.
21
+ # [2/5] Tests - Verify test files exist + run unit tests
22
+ # [3/5] PRD test steps - Execute testSteps commands from prd.json
23
+ # [4/5] API smoke test - Hit health endpoint (if configured)
24
+ # [5/5] Frontend smoke - Load page, check for errors (if configured)
25
+ #
26
+ # Pipeline stops at first failure to save time.
27
+ #
28
+ # ============================================================================
29
+ # FAILURE CONTEXT & LEARNING
30
+ # ============================================================================
31
+ # When verification fails, save_failure_context() ACCUMULATES errors across
32
+ # retries (not just the last failure). This lets Claude see patterns:
33
+ #
34
+ # === Attempt 1 failed for TASK-001 ===
35
+ # ERROR: relation "users" does not exist
36
+ # ---
37
+ # === Attempt 2 failed for TASK-001 ===
38
+ # ERROR: relation "users" does not exist
39
+ # ---
40
+ #
41
+ # Seeing "same error 3 times" signals a structural issue (missing migration,
42
+ # wrong prerequisites) rather than a simple bug to fix.
43
+ #
44
+ # Context is:
45
+ # - Appended per attempt (not overwritten)
46
+ # - Capped at 200 lines to avoid huge prompts
47
+ # - Cleared when switching to a new story
48
+ # - Cleared on success
49
+ #
50
+ # STRUCTURAL ERROR DETECTION:
51
+ # Some errors indicate structural issues (not code bugs) that can't be fixed
52
+ # by retrying. These are detected and flagged with actionable suggestions:
53
+ #
54
+ # - "column does not exist" → Suggest DB reset (schema mismatch)
55
+ # - "pending migration" → Suggest running migrations
56
+ # - "connection refused" → Suggest starting services
57
+ #
58
+ # This prevents infinite retry loops on issues that need manual intervention.
59
+ #
60
+ # ============================================================================
61
+ # CONFIGURATION (via .ralph/config.json)
62
+ # ============================================================================
63
+ #
64
+ # .checks.lint - Run linting (default: true)
65
+ # .checks.test - Run tests: true, false, or "final" (last story only)
66
+ # .checks.requireTests - Require test files for new code (default: false)
67
+ # .api.baseUrl - API URL for smoke tests
68
+ # .api.healthEndpoint - Health check path (default: /api/v1/health)
69
+ # .urls.frontend - Frontend URL for smoke tests
70
+ #
71
+ # ============================================================================
72
+ # MODULES
73
+ # ============================================================================
74
+ # verify/lint.sh - Linting and auto-fix (run_configured_checks)
75
+ # verify/tests.sh - Test existence + execution (verify_test_files_exist,
76
+ # run_unit_tests, verify_prd_criteria)
77
+ # verify/api.sh - API/frontend smoke tests (run_api_smoke_test,
78
+ # run_frontend_smoke_test)
79
+ #
80
+ # DEPENDENCIES: Requires utils.sh to be sourced first (for get_config, print_*)
81
+ #
82
+ # ============================================================================
83
+
84
+ # Source verification modules
85
+ VERIFY_DIR="${RALPH_LIB:-$(dirname "${BASH_SOURCE[0]}")}"
86
+ source "$VERIFY_DIR/verify/lint.sh"
87
+ source "$VERIFY_DIR/verify/tests.sh"
88
+ source "$VERIFY_DIR/verify/api.sh"
89
+
90
+ run_verification() {
91
+ local story="$1"
92
+
93
+ echo ""
94
+ print_info "=== Verification: $story ==="
95
+ echo ""
96
+
97
+ # Get story type for targeted checks
98
+ local story_type
99
+ story_type=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .type // "general"' "$RALPH_DIR/prd.json" 2>/dev/null)
100
+ export RALPH_STORY_TYPE="$story_type"
101
+
102
+ local failed=0
103
+
104
+ # ========================================
105
+ # STEP 1: Run lint checks
106
+ # ========================================
107
+ echo " [1/5] Running lint checks..."
108
+ if ! run_configured_checks "$story_type"; then
109
+ failed=1
110
+ fi
111
+
112
+ # ========================================
113
+ # STEP 2: Verify tests exist + run them
114
+ # ========================================
115
+ if [[ $failed -eq 0 ]]; then
116
+ echo ""
117
+ echo " [2/5] Running tests..."
118
+ # First check that test files exist for new code
119
+ if ! verify_test_files_exist; then
120
+ failed=1
121
+ elif ! run_unit_tests; then
122
+ failed=1
123
+ fi
124
+ fi
125
+
126
+ # ========================================
127
+ # STEP 3: Run PRD test steps
128
+ # ========================================
129
+ if [[ $failed -eq 0 ]]; then
130
+ echo ""
131
+ echo " [3/5] Running PRD test steps..."
132
+ if ! verify_prd_criteria "$story"; then
133
+ failed=1
134
+ fi
135
+ fi
136
+
137
+ # ========================================
138
+ # STEP 4: API smoke test (if configured)
139
+ # ========================================
140
+ if [[ $failed -eq 0 ]]; then
141
+ if ! run_api_smoke_test "$story"; then
142
+ failed=1
143
+ fi
144
+ fi
145
+
146
+ # ========================================
147
+ # STEP 5: Frontend smoke test (if configured)
148
+ # ========================================
149
+ if [[ $failed -eq 0 ]]; then
150
+ if ! run_frontend_smoke_test "$story"; then
151
+ failed=1
152
+ fi
153
+ fi
154
+
155
+ # ========================================
156
+ # Final result
157
+ # ========================================
158
+ echo ""
159
+ if [[ $failed -eq 0 ]]; then
160
+ print_success "=== All verification passed ==="
161
+ return 0
162
+ else
163
+ print_error "=== Verification failed ==="
164
+ save_failure_context "$story"
165
+ return 1
166
+ fi
167
+ }
168
+
169
+ # ============================================================================
170
+ # FAILURE CONTEXT
171
+ # ============================================================================
172
+ # Accumulates failure history across retries so Claude can identify patterns.
173
+ # If the same error appears multiple times, it's likely a structural issue
174
+ # (missing prerequisites, wrong approach) not a simple bug.
175
+ #
176
+ # Output format in .ralph/last_failure.txt:
177
+ # === Attempt 1 failed for STORY-ID ===
178
+ # <verification output>
179
+ # ---
180
+ # === Attempt 2 failed for STORY-ID ===
181
+ # <verification output>
182
+ # ---
183
+ #
184
+ save_failure_context() {
185
+ local story="$1"
186
+ local context_file="$RALPH_DIR/last_failure.txt"
187
+
188
+ # Get current attempt number from prd.json
189
+ local attempt
190
+ attempt=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .retryCount // 1' "$RALPH_DIR/prd.json" 2>/dev/null || echo "1")
191
+
192
+ # Append to failure history (not overwrite)
193
+ {
194
+ echo ""
195
+ echo "=== Attempt $attempt failed for $story ==="
196
+ echo ""
197
+ if [[ -f "$RALPH_DIR/last_verification.log" ]]; then
198
+ # Shorter excerpt per attempt since we're accumulating
199
+ tail -50 "$RALPH_DIR/last_verification.log"
200
+ fi
201
+ echo ""
202
+ echo "---"
203
+ } >> "$context_file"
204
+
205
+ # Cap file size - keep last ~200 lines to avoid huge prompts
206
+ if [[ -f "$context_file" ]]; then
207
+ local line_count
208
+ line_count=$(wc -l < "$context_file" | tr -d ' ')
209
+ if [[ $line_count -gt 200 ]]; then
210
+ tail -200 "$context_file" > "$context_file.tmp" && mv "$context_file.tmp" "$context_file"
211
+ fi
212
+ fi
213
+
214
+ # Detect structural errors and add actionable suggestions
215
+ _detect_structural_errors "$context_file"
216
+ }
217
+
218
+ # ============================================================================
219
+ # STRUCTURAL ERROR DETECTION
220
+ # ============================================================================
221
+ # Detects error patterns that indicate structural issues (not code bugs).
222
+ # These can't be fixed by retrying - they need specific actions like DB reset.
223
+ #
224
+ _detect_structural_errors() {
225
+ local context_file="$1"
226
+ [[ ! -f "$context_file" ]] && return
227
+
228
+ local error_content
229
+ error_content=$(cat "$context_file")
230
+
231
+ # Schema/column errors - suggest DB reset
232
+ # Only show if not already detected (avoid duplicate markers on retry)
233
+ if echo "$error_content" | grep -qiE "(column.*does not exist|relation.*does not exist|no such column|unknown column|undefined column)" && \
234
+ ! grep -q ">>> STRUCTURAL ISSUE: Database schema mismatch" "$context_file" 2>/dev/null; then
235
+ echo ""
236
+ print_warning "STRUCTURAL ISSUE DETECTED: Database schema mismatch"
237
+ echo ""
238
+ echo " The test database is missing columns/tables that the code expects."
239
+ echo " This usually happens when:"
240
+ echo " - Migrations were added but test DB wasn't reset"
241
+ echo " - Models were modified without running migrations"
242
+ echo ""
243
+ echo " SUGGESTED FIX (don't retry code - fix the schema):"
244
+ local reset_cmd
245
+ reset_cmd=$(get_config '.commands.resetDb' "")
246
+ if [[ -n "$reset_cmd" ]]; then
247
+ echo " $reset_cmd"
248
+ else
249
+ echo " # Add to .ralph/config.json:"
250
+ echo " {\"commands\": {\"resetDb\": \"npm run db:reset:test\"}}"
251
+ echo ""
252
+ echo " # Or run manually:"
253
+ echo " dropdb test_db && createdb test_db && alembic upgrade head"
254
+ fi
255
+ echo ""
256
+
257
+ # Append suggestion to failure context for Claude
258
+ {
259
+ echo ""
260
+ echo ">>> STRUCTURAL ISSUE: Database schema mismatch"
261
+ echo ">>> ACTION NEEDED: Reset test database, don't just retry code"
262
+ echo ">>> This is NOT a code bug - the test DB is missing schema changes"
263
+ } >> "$context_file"
264
+ fi
265
+
266
+ # Migration pending errors
267
+ if echo "$error_content" | grep -qiE "(pending migration|migrations are pending|migrate your database|alembic.*head)" && \
268
+ ! grep -q ">>> STRUCTURAL ISSUE: Pending migrations" "$context_file" 2>/dev/null; then
269
+ echo ""
270
+ print_warning "STRUCTURAL ISSUE DETECTED: Pending migrations"
271
+ echo ""
272
+ echo " Migrations need to be applied before tests can run."
273
+ echo ""
274
+ echo " SUGGESTED FIX:"
275
+ local migrate_cmd
276
+ migrate_cmd=$(get_config '.migrations.command' "alembic upgrade head")
277
+ echo " $migrate_cmd"
278
+ echo ""
279
+
280
+ {
281
+ echo ""
282
+ echo ">>> STRUCTURAL ISSUE: Pending migrations"
283
+ echo ">>> ACTION NEEDED: Run migrations before retrying"
284
+ } >> "$context_file"
285
+ fi
286
+
287
+ # Connection refused - service not running
288
+ if echo "$error_content" | grep -qiE "(connection refused|ECONNREFUSED|could not connect|connection error)" && \
289
+ ! grep -q ">>> STRUCTURAL ISSUE: Service not running" "$context_file" 2>/dev/null; then
290
+ echo ""
291
+ print_warning "STRUCTURAL ISSUE DETECTED: Service not running"
292
+ echo ""
293
+ echo " A required service (database, API, etc.) is not running."
294
+ echo ""
295
+ echo " SUGGESTED FIX:"
296
+ local dev_cmd
297
+ dev_cmd=$(get_config '.commands.dev' "docker compose up -d")
298
+ echo " $dev_cmd"
299
+ echo ""
300
+
301
+ {
302
+ echo ""
303
+ echo ">>> STRUCTURAL ISSUE: Service not running"
304
+ echo ">>> ACTION NEEDED: Start required services before retrying"
305
+ } >> "$context_file"
306
+ fi
307
+ }
package/ralph/loop.sh CHANGED
@@ -178,7 +178,12 @@ run_loop() {
178
178
  local iteration=0
179
179
  local last_story=""
180
180
  local consecutive_failures=0
181
- local max_story_retries=5
181
+ local consecutive_timeouts=0
182
+ local max_story_retries
183
+ local max_timeouts=5 # Skip after 5 consecutive timeouts (likely too large/complex)
184
+ # Default to 15 retries - generous enough for transient issues, catches infinite loops
185
+ # Override with config.json: "maxStoryRetries": 25
186
+ max_story_retries=$(get_config '.maxStoryRetries' "15")
182
187
  local total_attempts=0
183
188
  local skipped_stories=()
184
189
  local start_time
@@ -226,34 +231,58 @@ run_loop() {
226
231
 
227
232
  ((total_attempts++))
228
233
 
229
- # Track repeated failures on same story
234
+ # Track repeated failures on same story (also load from prd.json for restart persistence)
230
235
  if [[ "$story" == "$last_story" ]]; then
231
236
  ((consecutive_failures++))
232
-
233
- # Circuit breaker: skip to next story after max retries
234
- if [[ $consecutive_failures -gt $max_story_retries ]]; then
235
- print_error "Circuit breaker: $story failed $max_story_retries times, skipping to next story"
236
- echo ""
237
- echo " Saved failure context to: $RALPH_DIR/failures/$story.txt"
238
- mkdir -p "$RALPH_DIR/failures"
239
- cp "$RALPH_DIR/last_failure.txt" "$RALPH_DIR/failures/$story.txt" 2>/dev/null || true
240
- # Clear failure context so it doesn't leak into next story
241
- rm -f "$RALPH_DIR/last_failure.txt"
242
- skipped_stories+=("$story")
243
- # Mark as skipped (not passed, but move on)
244
- jq --arg id "$story" '(.stories[] | select(.id==$id)) |= . + {skipped: true}' "$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
245
- last_story=""
246
- consecutive_failures=0
247
- continue
248
- fi
249
-
250
- # Quick retry - no delay needed (Claude API isn't rate-limited)
251
- print_warning "Retry $consecutive_failures/$max_story_retries for $story"
252
237
  else
253
- consecutive_failures=1
238
+ # New story - clear failure history from previous story
239
+ rm -f "$RALPH_DIR/last_failure.txt"
240
+ # Load retry count from prd.json (persists across restarts)
241
+ consecutive_failures=$(jq -r --arg id "$story" '.stories[] | select(.id==$id) | .retryCount // 0' "$RALPH_DIR/prd.json")
242
+ consecutive_failures=$((consecutive_failures + 1))
243
+ consecutive_timeouts=0
254
244
  last_story="$story"
255
245
  fi
256
246
 
247
+ # Persist retry count to prd.json (survives restarts)
248
+ jq --arg id "$story" --argjson count "$consecutive_failures" \
249
+ '(.stories[] | select(.id==$id)) |= . + {retryCount: $count}' \
250
+ "$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
251
+
252
+ # Circuit breaker: skip to next story after max retries (prevents infinite loops)
253
+ # Note: This is NOT meant to stop legitimate retrying - 15 attempts is generous.
254
+ # If a story consistently fails after this many tries, it likely needs manual review
255
+ # (vague test steps, missing prerequisites, or fundamentally broken requirements).
256
+ if [[ $consecutive_failures -gt $max_story_retries ]]; then
257
+ print_error "Story $story has failed $consecutive_failures times - likely needs manual review"
258
+ echo ""
259
+ echo " This usually means:"
260
+ echo " - Test steps are too vague or ambiguous"
261
+ echo " - Missing prerequisites (DB setup, env vars, etc.)"
262
+ echo " - Story scope is too large - consider breaking it up"
263
+ echo ""
264
+ echo " Failure context saved to: $RALPH_DIR/failures/$story.txt"
265
+ mkdir -p "$RALPH_DIR/failures"
266
+ cp "$RALPH_DIR/last_failure.txt" "$RALPH_DIR/failures/$story.txt" 2>/dev/null || true
267
+ rm -f "$RALPH_DIR/last_failure.txt"
268
+ skipped_stories+=("$story")
269
+ jq --arg id "$story" '(.stories[] | select(.id==$id)) |= . + {skipped: true, skipReason: "exceeded max retries"}' "$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
270
+ last_story=""
271
+ consecutive_failures=0
272
+ continue
273
+ fi
274
+
275
+ # Show retry status (but don't make it scary - retrying is normal!)
276
+ if [[ $consecutive_failures -gt 1 ]]; then
277
+ if [[ $consecutive_failures -le 3 ]]; then
278
+ print_info "Attempt $consecutive_failures for $story (normal - refining solution)"
279
+ elif [[ $consecutive_failures -le 8 ]]; then
280
+ print_warning "Attempt $consecutive_failures/$max_story_retries for $story"
281
+ else
282
+ print_warning "Attempt $consecutive_failures/$max_story_retries for $story (getting close to limit)"
283
+ fi
284
+ fi
285
+
257
286
  # 2. Session startup checklist (skip on retries)
258
287
  [[ $consecutive_failures -gt 1 ]] && startup_checklist "true" || startup_checklist "false"
259
288
 
@@ -373,18 +402,42 @@ run_loop() {
373
402
  fi
374
403
 
375
404
  if [[ $claude_exit_code -ne 0 ]]; then
376
- print_warning "Claude session ended (timeout or error)"
377
- log_progress "$story" "TIMEOUT" "Claude session ended after ${timeout_seconds}s"
405
+ ((consecutive_timeouts++))
406
+ print_warning "Claude session ended (timeout or error) - timeout $consecutive_timeouts/$max_timeouts"
407
+ log_progress "$story" "TIMEOUT" "Claude session ended after ${timeout_seconds}s (timeout $consecutive_timeouts)"
378
408
  rm -f "$prompt_file"
379
409
 
380
410
  # Session may be broken - reset for next attempt
381
411
  session_started=false
382
412
 
413
+ # Skip on repeated timeouts (story is too large/complex for single session)
414
+ if [[ $consecutive_timeouts -ge $max_timeouts ]]; then
415
+ print_error "Story $story timed out $max_timeouts times - needs to be broken up"
416
+ echo ""
417
+ echo " Consecutive timeouts indicate the story is too large for a single"
418
+ echo " Claude session (${timeout_seconds}s). Consider:"
419
+ echo " - Breaking it into smaller, focused stories"
420
+ echo " - Increasing maxSessionSeconds in config.json"
421
+ echo ""
422
+ mkdir -p "$RALPH_DIR/failures"
423
+ echo "Story $story timed out $max_timeouts consecutive times (${timeout_seconds}s each)" > "$RALPH_DIR/failures/$story.txt"
424
+ echo "Consider breaking this story into smaller pieces." >> "$RALPH_DIR/failures/$story.txt"
425
+ skipped_stories+=("$story")
426
+ jq --arg id "$story" '(.stories[] | select(.id==$id)) |= . + {skipped: true, skipReason: "repeated timeouts"}' "$RALPH_DIR/prd.json" > "$RALPH_DIR/prd.json.tmp" && mv "$RALPH_DIR/prd.json.tmp" "$RALPH_DIR/prd.json"
427
+ last_story=""
428
+ consecutive_failures=0
429
+ consecutive_timeouts=0
430
+ continue
431
+ fi
432
+
383
433
  # If running specific story, exit on failure
384
434
  [[ -n "$specific_story" ]] && return 1
385
435
  continue
386
436
  fi
387
437
 
438
+ # Reset timeout counter on successful Claude run
439
+ consecutive_timeouts=0
440
+
388
441
  rm -f "$prompt_file"
389
442
  session_started=true # Mark session as active for subsequent stories
390
443
 
@@ -402,9 +455,9 @@ run_loop() {
402
455
  local verify_log="$RALPH_DIR/last_verification.log"
403
456
  set -o pipefail
404
457
  if run_verification "$story" 2>&1 | tee "$verify_log"; then
405
- # Mark story as complete
458
+ # Mark story as complete and reset retry count
406
459
  update_json "$RALPH_DIR/prd.json" \
407
- --arg id "$story" '(.stories[] | select(.id==$id) | .passes) = true'
460
+ --arg id "$story" '(.stories[] | select(.id==$id)) |= . + {passes: true, retryCount: 0}'
408
461
 
409
462
  # Clear failure context on success
410
463
  rm -f "$RALPH_DIR/last_failure.txt"