agentic-loop 3.17.0 → 3.17.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.17.0",
3
+ "version": "3.17.2",
4
4
  "description": "Autonomous AI coding loop - PRD-driven development with Claude Code",
5
5
  "author": "Allie Jones <allie@allthrive.ai>",
6
6
  "license": "MIT",
@@ -100,6 +100,7 @@ run_verification() {
100
100
  export RALPH_STORY_TYPE="$story_type"
101
101
 
102
102
  local failed=0
103
+ local failed_step=""
103
104
 
104
105
  # ========================================
105
106
  # STEP 1: Run lint checks
@@ -107,6 +108,7 @@ run_verification() {
107
108
  echo " [1/5] Running lint checks..."
108
109
  if ! run_configured_checks "$story_type"; then
109
110
  failed=1
111
+ failed_step="lint"
110
112
  fi
111
113
 
112
114
  # ========================================
@@ -118,8 +120,10 @@ run_verification() {
118
120
  # First check that test files exist for new code
119
121
  if ! verify_test_files_exist; then
120
122
  failed=1
123
+ failed_step="test files missing"
121
124
  elif ! run_unit_tests; then
122
125
  failed=1
126
+ failed_step="unit tests"
123
127
  fi
124
128
  fi
125
129
 
@@ -131,6 +135,7 @@ run_verification() {
131
135
  echo " [3/5] Running PRD test steps..."
132
136
  if ! verify_prd_criteria "$story"; then
133
137
  failed=1
138
+ failed_step="PRD test steps"
134
139
  fi
135
140
  fi
136
141
 
@@ -140,6 +145,7 @@ run_verification() {
140
145
  if [[ $failed -eq 0 ]]; then
141
146
  if ! run_api_smoke_test "$story"; then
142
147
  failed=1
148
+ failed_step="API smoke test"
143
149
  fi
144
150
  fi
145
151
 
@@ -149,6 +155,7 @@ run_verification() {
149
155
  if [[ $failed -eq 0 ]]; then
150
156
  if ! run_frontend_smoke_test "$story"; then
151
157
  failed=1
158
+ failed_step="frontend smoke test"
152
159
  fi
153
160
  fi
154
161
 
@@ -160,7 +167,7 @@ run_verification() {
160
167
  print_success "=== All verification passed ==="
161
168
  return 0
162
169
  else
163
- print_error "=== Verification failed ==="
170
+ print_error "=== Verification failed at: $failed_step ==="
164
171
  save_failure_context "$story"
165
172
  return 1
166
173
  fi
@@ -194,8 +201,20 @@ save_failure_context() {
194
201
  echo ""
195
202
  echo "=== Attempt $attempt failed for $story ==="
196
203
  echo ""
204
+ # Include migration failure if present (verification may not have run)
205
+ if [[ -f "$RALPH_DIR/last_migration_failure.log" ]]; then
206
+ echo "--- Migration Error ---"
207
+ tail -30 "$RALPH_DIR/last_migration_failure.log"
208
+ echo ""
209
+ fi
210
+ # Include pre-commit failure if present
211
+ if [[ -f "$RALPH_DIR/last_precommit_failure.log" ]]; then
212
+ echo "--- Pre-commit Error ---"
213
+ tail -30 "$RALPH_DIR/last_precommit_failure.log"
214
+ echo ""
215
+ fi
216
+ # Include verification output (lint, tests, API, etc.)
197
217
  if [[ -f "$RALPH_DIR/last_verification.log" ]]; then
198
- # Shorter excerpt per attempt since we're accumulating
199
218
  tail -50 "$RALPH_DIR/last_verification.log"
200
219
  fi
201
220
  echo ""
package/ralph/loop.sh CHANGED
@@ -97,9 +97,74 @@ preflight_checks() {
97
97
  echo "Aborted. Fix the issues and try again."
98
98
  exit 1
99
99
  fi
100
+ return 1 # Had warnings — don't cache this result
100
101
  fi
101
102
  }
102
103
 
104
+ # ============================================================================
105
+ # PREFLIGHT / PRD CACHE
106
+ # ============================================================================
107
+ # Caches preflight and PRD validation results so restarts within 10 minutes
108
+ # skip the slow connectivity checks and Claude auto-fix.
109
+ # Cache is invalidated by TTL expiry or config/PRD file changes (by hash).
110
+
111
+ _file_hash() {
112
+ [[ ! -f "$1" ]] && echo "no_file" && return
113
+ if command -v md5sum &>/dev/null; then
114
+ md5sum "$1" 2>/dev/null | cut -d' ' -f1
115
+ else
116
+ md5 -q "$1" 2>/dev/null
117
+ fi
118
+ }
119
+
120
+ _is_preflight_cached() {
121
+ local cache_file="$RALPH_DIR/.preflight_cache"
122
+ [[ ! -f "$cache_file" ]] && return 1
123
+
124
+ local cached_time cached_hash
125
+ read -r cached_time cached_hash < "$cache_file"
126
+
127
+ local now
128
+ now=$(date +%s)
129
+ [[ $(( now - cached_time )) -gt $PREFLIGHT_CACHE_TTL_SECONDS ]] && return 1
130
+
131
+ local config_hash
132
+ config_hash=$(_file_hash "$RALPH_DIR/config.json")
133
+ [[ "$cached_hash" != "$config_hash" ]] && return 1
134
+
135
+ return 0
136
+ }
137
+
138
+ _write_preflight_cache() {
139
+ local config_hash
140
+ config_hash=$(_file_hash "$RALPH_DIR/config.json")
141
+ echo "$(date +%s) $config_hash" > "$RALPH_DIR/.preflight_cache"
142
+ }
143
+
144
+ _is_prd_cached() {
145
+ local cache_file="$RALPH_DIR/.prd_validated"
146
+ [[ ! -f "$cache_file" ]] && return 1
147
+
148
+ local cached_time cached_hash
149
+ read -r cached_time cached_hash < "$cache_file"
150
+
151
+ local now
152
+ now=$(date +%s)
153
+ [[ $(( now - cached_time )) -gt $PREFLIGHT_CACHE_TTL_SECONDS ]] && return 1
154
+
155
+ local prd_hash
156
+ prd_hash=$(_file_hash "$RALPH_DIR/prd.json")
157
+ [[ "$cached_hash" != "$prd_hash" ]] && return 1
158
+
159
+ return 0
160
+ }
161
+
162
+ _write_prd_cache() {
163
+ local prd_hash
164
+ prd_hash=$(_file_hash "$RALPH_DIR/prd.json")
165
+ echo "$(date +%s) $prd_hash" > "$RALPH_DIR/.prd_validated"
166
+ }
167
+
103
168
  # Check if failure context is trivial (lint/format-only retries)
104
169
  # Returns 0 (trivial) if ALL error lines match trivial patterns
105
170
  _is_trivial_failure() {
@@ -341,7 +406,15 @@ run_loop() {
341
406
  check_dependencies
342
407
 
343
408
  # Pre-loop checks to catch issues before wasting iterations
344
- preflight_checks
409
+ if [[ "$fast_mode" == "true" ]]; then
410
+ print_info "Fast mode: skipping connectivity checks"
411
+ elif _is_preflight_cached; then
412
+ print_info "Pre-loop checks passed recently, skipping"
413
+ else
414
+ if preflight_checks; then
415
+ _write_preflight_cache
416
+ fi
417
+ fi
345
418
 
346
419
  if [[ ! -f "$RALPH_DIR/prd.json" ]]; then
347
420
  # Check for misplaced PRD in subdirectories
@@ -383,8 +456,17 @@ run_loop() {
383
456
  fi
384
457
 
385
458
  # Validate PRD structure
386
- if ! validate_prd "$RALPH_DIR/prd.json"; then
387
- return 1
459
+ if [[ "$fast_mode" == "true" ]]; then
460
+ print_info "Fast mode: structural PRD check only"
461
+ validate_prd "$RALPH_DIR/prd.json" "true" || return 1
462
+ elif _is_prd_cached; then
463
+ print_info "PRD validated recently, structural check only"
464
+ validate_prd "$RALPH_DIR/prd.json" "true" || return 1
465
+ else
466
+ if ! validate_prd "$RALPH_DIR/prd.json"; then
467
+ return 1
468
+ fi
469
+ _write_prd_cache
388
470
  fi
389
471
 
390
472
  local iteration=0
@@ -481,14 +563,15 @@ run_loop() {
481
563
  if [[ $consecutive_failures -gt $max_story_retries ]]; then
482
564
  print_error "Story $story has failed $consecutive_failures times - stopping loop"
483
565
  echo ""
484
- echo " This usually means:"
485
- echo " - Test steps are too vague or ambiguous"
486
- echo " - Missing prerequisites (DB setup, env vars, etc.)"
487
- echo " - Story scope is too large - consider breaking it up"
488
- echo ""
489
- echo " Failure context saved to: $RALPH_DIR/failures/$story.txt"
490
566
  mkdir -p "$RALPH_DIR/failures"
491
567
  cp "$RALPH_DIR/last_failure.txt" "$RALPH_DIR/failures/$story.txt" 2>/dev/null || true
568
+ # Show the actual last error instead of generic guesses
569
+ if [[ -f "$RALPH_DIR/last_failure.txt" ]]; then
570
+ echo " Last failure:"
571
+ tail -20 "$RALPH_DIR/last_failure.txt" | sed 's/^/ /'
572
+ fi
573
+ echo ""
574
+ echo " Full failure context saved to: $RALPH_DIR/failures/$story.txt"
492
575
  local passed failed
493
576
  passed=$(jq '[.stories[] | select(.passes==true)] | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
494
577
  failed=$(jq '[.stories[] | select(.passes==false)] | length' "$RALPH_DIR/prd.json" 2>/dev/null || echo "0")
@@ -632,18 +715,22 @@ run_loop() {
632
715
  break
633
716
  done
634
717
 
635
- rm -f "$claude_output_log"
636
-
637
718
  if [[ $crash_attempt -ge $max_crash_retries ]]; then
638
719
  echo ""
639
720
  print_warning "Claude API unavailable after $max_crash_retries attempts"
721
+ if [[ -f "$claude_output_log" ]]; then
722
+ echo " Last error:"
723
+ tail -5 "$claude_output_log" | sed 's/^/ /'
724
+ fi
640
725
  print_info "Waiting 60s before retrying... (Ctrl+C to stop, then 'npx agentic-loop run' to restart)"
641
726
  log_progress "$story" "CLI_CRASH" "API unavailable, waiting 60s before next iteration"
642
- rm -f "$prompt_file"
727
+ rm -f "$prompt_file" "$claude_output_log"
643
728
  sleep 60 # Longer cooldown before retrying
644
729
  continue # Continue main loop instead of stopping
645
730
  fi
646
731
 
732
+ rm -f "$claude_output_log"
733
+
647
734
  if [[ $claude_exit_code -ne 0 ]]; then
648
735
  ((consecutive_timeouts++))
649
736
  print_warning "Claude session ended (timeout or error) - timeout $consecutive_timeouts/$max_timeouts"
@@ -709,6 +796,8 @@ run_loop() {
709
796
 
710
797
  # Clear failure context on success
711
798
  rm -f "$RALPH_DIR/last_failure.txt"
799
+ rm -f "$RALPH_DIR"/last_*_failure.log
800
+ rm -f "$RALPH_DIR"/last_*_check.log
712
801
  rm -f "$RALPH_DIR/last_verification.log"
713
802
 
714
803
  # Get story title for commit message and completion display
@@ -102,6 +102,7 @@
102
102
  # Returns 0 if valid (possibly after auto-fix), 1 if unrecoverable error
103
103
  validate_prd() {
104
104
  local prd_file="$1"
105
+ local dry_run="${2:-false}"
105
106
 
106
107
  # Check file exists
107
108
  if [[ ! -f "$prd_file" ]]; then
@@ -219,15 +220,17 @@ validate_prd() {
219
220
  echo ""
220
221
  fi
221
222
 
222
- # Validate API smoke test configuration
223
- _validate_api_config "$config"
223
+ # Validate API smoke test configuration (skip in fast/cached mode)
224
+ if [[ "$dry_run" != "true" ]]; then
225
+ _validate_api_config "$config"
226
+ fi
224
227
 
225
228
  # Replace hardcoded paths with config placeholders
226
229
  fix_hardcoded_paths "$prd_file" "$config"
227
230
 
228
231
  # Validate and fix individual stories
229
- # $2 is optional dry_run flag — when "true", skip auto-fix
230
- _validate_and_fix_stories "$prd_file" "${2:-}" || return 1
232
+ # dry_run flag — when "true", skip auto-fix
233
+ _validate_and_fix_stories "$prd_file" "$dry_run" || return 1
231
234
 
232
235
  return 0
233
236
  }
@@ -323,6 +326,7 @@ _validate_and_fix_stories() {
323
326
  local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0 cnt_frontend_mcp=0
324
327
  local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
325
328
  local cnt_migration_prereq=0 cnt_naming_convention=0 cnt_bare_pytest=0
329
+ local cnt_server_only=0
326
330
  local cnt_custom=0
327
331
 
328
332
  echo " Checking test coverage..."
@@ -471,6 +475,32 @@ _validate_and_fix_stories() {
471
475
  fi
472
476
  fi
473
477
 
478
+ # Check 9: Stories where ALL testSteps depend on a live server
479
+ # If every testStep is a curl/wget/httpie command and none are offline
480
+ # (npm test, pytest, tsc, playwright, cargo test, go test, etc.),
481
+ # the story will always fail without a running server.
482
+ if [[ -n "$test_steps" ]]; then
483
+ local has_offline_step=false
484
+ local has_server_step=false
485
+ local step_list
486
+ step_list=$(jq -r --arg id "$story_id" \
487
+ '.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
488
+
489
+ while IFS= read -r single_step; do
490
+ [[ -z "$single_step" ]] && continue
491
+ if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
492
+ has_server_step=true
493
+ else
494
+ has_offline_step=true
495
+ fi
496
+ done <<< "$step_list"
497
+
498
+ if [[ "$has_server_step" == "true" && "$has_offline_step" == "false" ]]; then
499
+ story_issues+="all testSteps need a live server (add offline test: npm test, tsc --noEmit, pytest), "
500
+ cnt_server_only=$((cnt_server_only + 1))
501
+ fi
502
+ fi
503
+
474
504
  # Snapshot built-in issues before custom checks append
475
505
  local builtin_story_issues="$story_issues"
476
506
 
@@ -517,6 +547,7 @@ _validate_and_fix_stories() {
517
547
  [[ $cnt_migration_prereq -gt 0 ]] && echo " ${cnt_migration_prereq}x migration: add prerequisites (DB reset)"
518
548
  [[ $cnt_naming_convention -gt 0 ]] && echo " ${cnt_naming_convention}x API consumer: add camelCase transformation note"
519
549
  [[ $cnt_bare_pytest -gt 0 ]] && echo " ${cnt_bare_pytest}x use 'uv run pytest' not bare 'pytest'"
550
+ [[ $cnt_server_only -gt 0 ]] && echo " ${cnt_server_only}x all testSteps need live server (add offline fallback)"
520
551
  [[ $cnt_custom -gt 0 ]] && echo " ${cnt_custom} stories with custom check issues"
521
552
 
522
553
  # Skip auto-fix in dry-run mode
@@ -630,6 +661,10 @@ RULES:
630
661
  Example: \"notes\": \"Transform API responses from snake_case to camelCase. Create typed interfaces with camelCase properties and map: const user = { userName: data.user_name }\"
631
662
  10. Each story should include its own techStack and constraints fields. Do NOT add these at the PRD root level.
632
663
  Move any root-level techStack, globalConstraints, originalContext, testing, architecture, or testUsers into the relevant stories.
664
+ 11. Stories where ALL testSteps are curl commands MUST also include at least one offline test step
665
+ that can verify code correctness without a running server.
666
+ Examples: \"npm test\", \"npx tsc --noEmit\", \"pytest tests/unit/\", \"go test ./...\"
667
+ This prevents wasted retries when the server isn't running.
633
668
 
634
669
  CURRENT PRD:
635
670
  $(cat "$prd_file")
@@ -788,6 +823,24 @@ validate_stories_quick() {
788
823
  fi
789
824
  fi
790
825
  fi
826
+
827
+ # Check 8: All testSteps are server-dependent
828
+ if [[ -n "$test_steps" ]]; then
829
+ local has_offline=false has_server=false
830
+ local steps
831
+ steps=$(jq -r --arg id "$story_id" \
832
+ '.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
833
+ while IFS= read -r s; do
834
+ [[ -z "$s" ]] && continue
835
+ if echo "$s" | grep -qE "^(curl |wget |http )"; then
836
+ has_server=true
837
+ else
838
+ has_offline=true
839
+ fi
840
+ done <<< "$steps"
841
+ [[ "$has_server" == "true" && "$has_offline" == "false" ]] && \
842
+ issues+="$story_id: all testSteps need live server, "
843
+ fi
791
844
  done <<< "$story_ids"
792
845
 
793
846
  echo "$issues"
package/ralph/test.sh CHANGED
@@ -134,11 +134,15 @@ run_all_prd_tests() {
134
134
  [[ -z "$step" ]] && continue
135
135
  ((total++))
136
136
 
137
- echo -n " $step... "
137
+ # Expand config placeholders (e.g., {config.urls.backend})
138
+ local expanded_step
139
+ expanded_step=$(_expand_config_vars "$step")
140
+
141
+ echo -n " $expanded_step... "
138
142
 
139
143
  local step_log
140
144
  step_log=$(mktemp)
141
- if safe_exec "$step" "$step_log"; then
145
+ if safe_exec "$expanded_step" "$step_log"; then
142
146
  print_success "passed"
143
147
  ((passed++))
144
148
  else
package/ralph/utils.sh CHANGED
@@ -22,6 +22,7 @@ readonly BROWSER_TIMEOUT_SECONDS=60
22
22
  readonly BROWSER_PAGE_TIMEOUT_MS=30000
23
23
  readonly CURL_TIMEOUT_SECONDS=10
24
24
  readonly SIGN_EXTRACTION_TIMEOUT_SECONDS=30
25
+ readonly PREFLIGHT_CACHE_TTL_SECONDS=600
25
26
 
26
27
  # Common project directories (avoid duplication across files)
27
28
  readonly FRONTEND_DIRS=("apps/web" "frontend" "client" "web")
@@ -226,8 +226,11 @@ run_unit_tests() {
226
226
  }
227
227
 
228
228
  # Expand config placeholders in a string
229
- # Usage: expand_config_vars "curl {config.urls.backend}/api"
230
- # Expands: {config.urls.backend}, {config.urls.frontend}, {config.directories.*}
229
+ # Usage: _expand_config_vars "curl {config.urls.backend}/api"
230
+ # Expands any {config.X.Y} placeholder from .ralph/config.json via jq.
231
+ # Known placeholders have fallback paths for backward compatibility:
232
+ # {config.urls.backend} -> .urls.backend // .api.baseUrl
233
+ # {config.urls.frontend} -> .urls.frontend // .testUrlBase
231
234
  _expand_config_vars() {
232
235
  local input="$1"
233
236
  local config="$RALPH_DIR/config.json"
@@ -237,41 +240,38 @@ _expand_config_vars() {
237
240
 
238
241
  local result="$input"
239
242
 
240
- # Expand {config.urls.backend}
243
+ # Known placeholders with backward-compatible fallback paths
241
244
  if [[ "$result" == *"{config.urls.backend}"* ]]; then
242
- local backend_url
243
- backend_url=$(jq -r '.urls.backend // .api.baseUrl // empty' "$config" 2>/dev/null)
244
- if [[ -n "$backend_url" ]]; then
245
- result="${result//\{config.urls.backend\}/$backend_url}"
246
- fi
245
+ local val
246
+ val=$(jq -r '.urls.backend // .api.baseUrl // empty' "$config" 2>/dev/null)
247
+ [[ -n "$val" ]] && result="${result//\{config.urls.backend\}/$val}"
247
248
  fi
248
249
 
249
- # Expand {config.urls.frontend}
250
250
  if [[ "$result" == *"{config.urls.frontend}"* ]]; then
251
- local frontend_url
252
- frontend_url=$(jq -r '.urls.frontend // .testUrlBase // empty' "$config" 2>/dev/null)
253
- if [[ -n "$frontend_url" ]]; then
254
- result="${result//\{config.urls.frontend\}/$frontend_url}"
255
- fi
256
- fi
257
-
258
- # Expand {config.directories.backend}
259
- if [[ "$result" == *"{config.directories.backend}"* ]]; then
260
- local backend_dir
261
- backend_dir=$(jq -r '.directories.backend // empty' "$config" 2>/dev/null)
262
- if [[ -n "$backend_dir" ]]; then
263
- result="${result//\{config.directories.backend\}/$backend_dir}"
264
- fi
251
+ local val
252
+ val=$(jq -r '.urls.frontend // .testUrlBase // empty' "$config" 2>/dev/null)
253
+ [[ -n "$val" ]] && result="${result//\{config.urls.frontend\}/$val}"
265
254
  fi
266
255
 
267
- # Expand {config.directories.frontend}
268
- if [[ "$result" == *"{config.directories.frontend}"* ]]; then
269
- local frontend_dir
270
- frontend_dir=$(jq -r '.directories.frontend // empty' "$config" 2>/dev/null)
271
- if [[ -n "$frontend_dir" ]]; then
272
- result="${result//\{config.directories.frontend\}/$frontend_dir}"
256
+ # Generic expansion for any remaining {config.X.Y.Z} placeholders
257
+ # Handles {config.urls.app}, {config.api.healthEndpoint}, {config.directories.*}, etc.
258
+ local max_expansions=10
259
+ while [[ "$result" =~ \{config\.([a-zA-Z0-9_.]+)\} ]] && [[ $max_expansions -gt 0 ]]; do
260
+ local placeholder="${BASH_REMATCH[0]}"
261
+ local config_path="${BASH_REMATCH[1]}"
262
+ local jq_path=".${config_path}"
263
+
264
+ local val
265
+ val=$(jq -r "$jq_path // empty" "$config" 2>/dev/null)
266
+ if [[ -n "$val" ]]; then
267
+ result="${result//$placeholder/$val}"
268
+ else
269
+ # Unresolvable — warn and stop to avoid infinite loop
270
+ print_warning "Unresolved config placeholder: $placeholder (key '$config_path' not in config.json)" >&2
271
+ break
273
272
  fi
274
- fi
273
+ ((max_expansions--))
274
+ done
275
275
 
276
276
  echo "$result"
277
277
  }