agentic-loop 3.19.0 → 3.21.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 (51) hide show
  1. package/.claude/commands/tour.md +11 -7
  2. package/.claude/commands/vibe-help.md +5 -2
  3. package/.claude/commands/vibe-list.md +17 -2
  4. package/.claude/skills/prd/SKILL.md +21 -6
  5. package/.claude/skills/setup-review/SKILL.md +56 -0
  6. package/.claude/skills/tour/SKILL.md +11 -7
  7. package/.claude/skills/vibe-help/SKILL.md +2 -1
  8. package/.claude/skills/vibe-list/SKILL.md +5 -2
  9. package/.pre-commit-hooks.yaml +8 -0
  10. package/README.md +4 -0
  11. package/bin/agentic-loop.sh +7 -0
  12. package/bin/ralph.sh +29 -0
  13. package/dist/checks/check-signs-secrets.d.ts +9 -0
  14. package/dist/checks/check-signs-secrets.d.ts.map +1 -0
  15. package/dist/checks/check-signs-secrets.js +57 -0
  16. package/dist/checks/check-signs-secrets.js.map +1 -0
  17. package/dist/checks/index.d.ts +2 -5
  18. package/dist/checks/index.d.ts.map +1 -1
  19. package/dist/checks/index.js +4 -9
  20. package/dist/checks/index.js.map +1 -1
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/package.json +2 -1
  26. package/ralph/hooks/common.sh +47 -0
  27. package/ralph/hooks/warn-debug.sh +12 -26
  28. package/ralph/hooks/warn-empty-catch.sh +21 -34
  29. package/ralph/hooks/warn-secrets.sh +39 -52
  30. package/ralph/hooks/warn-urls.sh +25 -45
  31. package/ralph/init.sh +58 -82
  32. package/ralph/loop.sh +506 -53
  33. package/ralph/prd-check.sh +177 -236
  34. package/ralph/prd.sh +5 -2
  35. package/ralph/setup/quick-setup.sh +2 -16
  36. package/ralph/setup.sh +68 -80
  37. package/ralph/signs.sh +8 -0
  38. package/ralph/uat.sh +2015 -0
  39. package/ralph/utils.sh +198 -69
  40. package/ralph/verify/tests.sh +65 -10
  41. package/templates/PROMPT.md +10 -4
  42. package/templates/UAT-PROMPT.md +197 -0
  43. package/templates/config/elixir.json +0 -2
  44. package/templates/config/fastmcp.json +0 -2
  45. package/templates/config/fullstack.json +2 -4
  46. package/templates/config/go.json +0 -2
  47. package/templates/config/minimal.json +0 -2
  48. package/templates/config/node.json +0 -2
  49. package/templates/config/python.json +0 -2
  50. package/templates/config/rust.json +0 -2
  51. package/templates/prd-example.json +6 -8
package/ralph/utils.sh CHANGED
@@ -2,6 +2,18 @@
2
2
  # shellcheck shell=bash
3
3
  # utils.sh - Shared utility functions for ralph
4
4
 
5
+ # Get utils.sh directory to locate package.json
6
+ _UTILS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ _PACKAGE_JSON="$_UTILS_DIR/../package.json"
8
+
9
+ # Version constant (read from package.json)
10
+ RALPH_VERSION="$(jq -r '.version' "$_PACKAGE_JSON" 2>/dev/null || echo "unknown")"
11
+ readonly RALPH_VERSION
12
+
13
+ # Loop protocol version (for stream-json activity feed)
14
+ RALPH_LOOP_VERSION="2"
15
+ readonly RALPH_LOOP_VERSION
16
+
5
17
  # Constants - Output limits
6
18
  readonly MAX_LOG_LINES=30
7
19
  readonly MAX_PROGRESS_LINES=10
@@ -16,7 +28,6 @@ readonly MAX_SIGN_DEDUP_EXISTING=20
16
28
  # Constants - Timeouts (centralized to avoid magic numbers)
17
29
  readonly ITERATION_DELAY_SECONDS=0
18
30
  readonly DEFAULT_TIMEOUT_SECONDS=600
19
- readonly DEFAULT_MAX_ITERATIONS=20
20
31
  readonly CODE_REVIEW_TIMEOUT_SECONDS=120
21
32
  readonly BROWSER_TIMEOUT_SECONDS=60
22
33
  readonly BROWSER_PAGE_TIMEOUT_MS=30000
@@ -140,38 +151,24 @@ get_config() {
140
151
  echo "$default"
141
152
  }
142
153
 
143
- # Get a field from a story in prd.json
144
- # Usage: get_story_field "STORY-001" ".type" "general"
145
- get_story_field() {
146
- local story_id="$1"
147
- local field="$2"
148
- local default="${3:-}"
149
- local prd="$RALPH_DIR/prd.json"
150
-
151
- if [[ -f "$prd" ]]; then
152
- local value
153
- value=$(jq -r --arg id "$story_id" ".stories[] | select(.id==\$id) | $field // empty" "$prd" 2>/dev/null)
154
- if [[ -n "$value" && "$value" != "null" ]]; then
155
- echo "$value"
156
- return
154
+ # Migrate deprecated config fields in-place
155
+ # Safe to call multiple times - only writes if changes are needed
156
+ _migrate_config() {
157
+ local config="$RALPH_DIR/config.json"
158
+ [[ ! -f "$config" ]] && return 0
159
+
160
+ # testUrlBase -> urls.frontend
161
+ local legacy_url
162
+ legacy_url=$(jq -r '.testUrlBase // empty' "$config" 2>/dev/null)
163
+ if [[ -n "$legacy_url" ]]; then
164
+ local current_frontend
165
+ current_frontend=$(jq -r '.urls.frontend // empty' "$config" 2>/dev/null)
166
+ if [[ -z "$current_frontend" ]]; then
167
+ jq --arg url "$legacy_url" '.urls.frontend = $url | del(.testUrlBase)' "$config" > "${config}.tmp" && mv "${config}.tmp" "$config"
168
+ else
169
+ jq 'del(.testUrlBase)' "$config" > "${config}.tmp" && mv "${config}.tmp" "$config"
157
170
  fi
158
171
  fi
159
- echo "$default"
160
- }
161
-
162
- # Clear a failure log file
163
- # Usage: clear_failure_log "lint" # clears last_lint_failure.log
164
- clear_failure_log() {
165
- local log_name="$1"
166
- rm -f "$RALPH_DIR/last_${log_name}_failure.log"
167
- }
168
-
169
- # Append content to a failure log file
170
- # Usage: log_failure "lint" "Error details here"
171
- log_failure() {
172
- local log_name="$1"
173
- local content="$2"
174
- echo "$content" >> "$RALPH_DIR/last_${log_name}_failure.log"
175
172
  }
176
173
 
177
174
  # Deep merge user config with project config
@@ -431,33 +428,17 @@ validate_command() {
431
428
 
432
429
  # Block obviously dangerous patterns (defense-in-depth, not security boundary)
433
430
  local dangerous_patterns=(
434
- # Destructive file operations
435
431
  'rm[[:space:]]+-rf[[:space:]]+/' # rm -rf /
436
432
  'rm[[:space:]]+-rf[[:space:]]+~' # rm -rf ~ (home dir)
437
- 'rm[[:space:]]+-rf[[:space:]]+\*' # rm -rf *
438
- 'rm[[:space:]]+-rf[[:space:]]+\.\.' # rm -rf ..
439
433
  'rm[[:space:]].*--no-preserve-root' # rm with --no-preserve-root
440
- # Remote code execution
441
434
  'curl.*\|.*bash' # curl | bash
442
435
  'curl.*\|.*sh[[:space:]]*$' # curl | sh
443
436
  'wget.*\|.*bash' # wget | bash
444
437
  'wget.*\|.*sh[[:space:]]*$' # wget | sh
445
- 'curl.*>[[:space:]]*/tmp/.*&&.*bash' # curl > /tmp/x && bash
446
- # Code injection
447
- '\$\([^)]*eval' # $(eval ...)
448
438
  'eval[[:space:]]+\$' # eval $var
449
- 'eval[[:space:]]+["\x27]' # eval "..." or eval '...'
450
- # System damage
451
439
  '>[[:space:]]*/dev/sd' # write to disk devices
452
440
  '>[[:space:]]*/dev/nvme' # write to nvme devices
453
441
  'mkfs\.' # format filesystems
454
- 'dd[[:space:]]+if=' # dd commands
455
- ':(){:|:&};:' # fork bomb
456
- # Credential theft
457
- 'cat.*\.ssh/id_' # read SSH keys
458
- 'cat.*/etc/shadow' # read shadow file
459
- 'cat.*\.aws/credentials' # read AWS creds
460
- 'cat.*\.env' # read env files (often has secrets)
461
442
  )
462
443
 
463
444
  for pattern in "${dangerous_patterns[@]}"; do
@@ -471,25 +452,6 @@ validate_command() {
471
452
  return 0
472
453
  }
473
454
 
474
- # Validate a URL is safe (http/https only, no internal IPs in production)
475
- validate_url() { # public-api
476
- local url="$1"
477
-
478
- # Must start with http:// or https://
479
- if [[ ! "$url" =~ ^https?:// ]]; then
480
- print_error "Invalid URL scheme (must be http or https): $url"
481
- return 1
482
- fi
483
-
484
- # Block file:// and other dangerous schemes
485
- if [[ "$url" =~ ^(file|ftp|data|javascript): ]]; then
486
- print_error "Dangerous URL scheme: $url"
487
- return 1
488
- fi
489
-
490
- return 0
491
- }
492
-
493
455
  # Safely execute a command (validates first, uses bash -c instead of eval)
494
456
  safe_exec() {
495
457
  local cmd="$1"
@@ -710,6 +672,92 @@ fix_hardcoded_paths() {
710
672
  fi
711
673
  }
712
674
 
675
+ # Detect the type of project based on files present
676
+ # Returns one of: fullstack, rust, go, elixir, hugo, fastmcp, django, fastapi, python, node, minimal
677
+ detect_project_type() {
678
+ # Check for fullstack patterns first (more specific)
679
+ if [[ -d "frontend" && -d "core" ]]; then
680
+ echo "fullstack"; return
681
+ elif [[ -d "frontend" && -d "backend" ]]; then
682
+ echo "fullstack"; return
683
+ elif [[ -d "apps" ]]; then
684
+ echo "fullstack"; return
685
+ fi
686
+
687
+ # Single-language projects
688
+ if [[ -f "Cargo.toml" ]]; then echo "rust"; return; fi
689
+ if [[ -f "go.mod" ]]; then echo "go"; return; fi
690
+ if [[ -f "mix.exs" ]]; then echo "elixir"; return; fi
691
+
692
+ # Hugo (Go static site generator)
693
+ for _hc in "hugo.toml" "hugo.yaml" "hugo.json" "config.toml"; do
694
+ if [[ -f "$_hc" ]] && [[ -d "content" || -d "layouts" || -d "themes" ]]; then
695
+ echo "hugo"; return
696
+ fi
697
+ done
698
+
699
+ # Python framework variants (most specific first)
700
+ if [[ -f "pyproject.toml" ]]; then
701
+ if grep -qiE "(fastmcp|\"fastmcp\"|'fastmcp')" pyproject.toml 2>/dev/null; then
702
+ echo "fastmcp"; return
703
+ elif grep -qiE "(django|\"django\"|'django')" pyproject.toml 2>/dev/null || [[ -f "manage.py" ]]; then
704
+ echo "django"; return
705
+ elif grep -qiE "(fastapi|\"fastapi\"|'fastapi')" pyproject.toml 2>/dev/null; then
706
+ echo "fastapi"; return
707
+ else
708
+ echo "python"; return
709
+ fi
710
+ elif [[ -f "requirements.txt" || -f "setup.py" ]]; then
711
+ if [[ -f "requirements.txt" ]]; then
712
+ if grep -qi 'fastmcp' requirements.txt 2>/dev/null; then echo "fastmcp"; return; fi
713
+ if grep -qi 'django' requirements.txt 2>/dev/null || [[ -f "manage.py" ]]; then echo "django"; return; fi
714
+ if grep -qi 'fastapi' requirements.txt 2>/dev/null; then echo "fastapi"; return; fi
715
+ fi
716
+ echo "python"; return
717
+ elif [[ -f "manage.py" ]]; then
718
+ echo "django"; return
719
+ fi
720
+
721
+ if [[ -f "package.json" ]]; then echo "node"; return; fi
722
+
723
+ echo "minimal"
724
+ }
725
+
726
+ # Detect framework type for CLAUDE.md generation
727
+ # Returns one of: fastmcp, fastapi, django, react, or empty string
728
+ detect_framework_type() {
729
+ if [[ -f "pyproject.toml" ]]; then
730
+ if grep -qiE "(fastmcp|\"fastmcp\"|'fastmcp')" pyproject.toml 2>/dev/null; then
731
+ echo "fastmcp"; return
732
+ elif grep -qiE "(fastapi|\"fastapi\"|'fastapi')" pyproject.toml 2>/dev/null; then
733
+ echo "fastapi"; return
734
+ elif grep -qiE "(django|\"django\"|'django')" pyproject.toml 2>/dev/null || [[ -f "manage.py" ]]; then
735
+ echo "django"; return
736
+ fi
737
+ elif [[ -f "requirements.txt" ]]; then
738
+ if grep -qi 'fastmcp' requirements.txt 2>/dev/null; then echo "fastmcp"; return; fi
739
+ if grep -qi 'fastapi' requirements.txt 2>/dev/null; then echo "fastapi"; return; fi
740
+ if grep -qi 'django' requirements.txt 2>/dev/null || [[ -f "manage.py" ]]; then echo "django"; return; fi
741
+ elif [[ -f "manage.py" ]]; then
742
+ echo "django"; return
743
+ fi
744
+
745
+ # Check for React in frontend package.json
746
+ local pkg="package.json"
747
+ local fe_dir=""
748
+ [[ -d "frontend" ]] && fe_dir="frontend"
749
+ [[ -d "client" ]] && fe_dir="client"
750
+ [[ -d "web" ]] && fe_dir="web"
751
+ [[ -d "apps/web" ]] && fe_dir="apps/web"
752
+ [[ -n "$fe_dir" && -f "${fe_dir}/package.json" ]] && pkg="${fe_dir}/package.json"
753
+
754
+ if [[ -f "$pkg" ]] && grep -q '"react"' "$pkg" 2>/dev/null; then
755
+ echo "react"; return
756
+ fi
757
+
758
+ echo ""
759
+ }
760
+
713
761
  # Detect Python runner (uv, poetry, pipenv, or plain python)
714
762
  detect_python_runner() {
715
763
  local search_dir="${1:-.}"
@@ -732,7 +780,7 @@ detect_python_runner() {
732
780
  return 0
733
781
  fi
734
782
 
735
- # Default to plain command (assumes activated venv or global)
783
+ # Default to plain command (no runner detected)
736
784
  echo ""
737
785
  return 0
738
786
  }
@@ -761,9 +809,11 @@ detect_migration_tool() {
761
809
  return 0
762
810
  fi
763
811
 
764
- # Django
812
+ # Django (use python3 for macOS compatibility when no runner detected)
765
813
  if [[ -f "$search_dir/manage.py" ]] && [[ -d "$search_dir" ]] && find "$search_dir" -type d -name "migrations" -print -quit | grep -q .; then
766
- echo "cd $search_dir && ${py_runner}${py_runner:+ }python manage.py migrate"
814
+ local python_cmd="python3"
815
+ [[ -n "$py_runner" ]] && python_cmd="python"
816
+ echo "cd $search_dir && ${py_runner}${py_runner:+ }${python_cmd} manage.py migrate"
767
817
  return 0
768
818
  fi
769
819
 
@@ -812,6 +862,85 @@ find_all_migration_tools() {
812
862
  printf '%s\n' "${tools[@]}" | sort -u
813
863
  }
814
864
 
865
+ # Validate batch assignments in a PRD file
866
+ # Returns 0 if valid (or no batches), 1 with error messages if invalid
867
+ # Checks: all-or-none batch field, dependency ordering, file overlap within batch
868
+ validate_batch_assignments() {
869
+ local prd_file="$1"
870
+ local errors=""
871
+ local error_count=0
872
+
873
+ # Count stories with and without batch
874
+ local total_stories with_batch without_batch
875
+ total_stories=$(jq '.stories | length' "$prd_file" 2>/dev/null || echo "0")
876
+ with_batch=$(jq '[.stories[] | select(.batch != null)] | length' "$prd_file" 2>/dev/null || echo "0")
877
+ without_batch=$((total_stories - with_batch))
878
+
879
+ # No batches at all — valid, nothing to check
880
+ [[ "$with_batch" -eq 0 ]] && return 0
881
+
882
+ # Some but not all stories have batch — error
883
+ if [[ "$without_batch" -gt 0 ]]; then
884
+ local missing_ids
885
+ missing_ids=$(jq -r '[.stories[] | select(.batch == null) | .id] | join(", ")' "$prd_file" 2>/dev/null)
886
+ errors+="batch field missing on: $missing_ids\n"
887
+ error_count=$((error_count + 1))
888
+ fi
889
+
890
+ # Check dependency ordering: no dependsOn pointing to same or later batch
891
+ local dep_violations
892
+ dep_violations=$(jq -r '
893
+ .stories as $all |
894
+ [.stories[] | . as $s |
895
+ ($s.dependsOn // [])[] as $dep |
896
+ ($all[] | select(.id == $dep)) as $dep_story |
897
+ select($dep_story.batch != null and $s.batch != null and $dep_story.batch >= $s.batch) |
898
+ "\($s.id) (batch \($s.batch)) depends on \($dep) (batch \($dep_story.batch))"
899
+ ] | .[]' "$prd_file" 2>/dev/null)
900
+
901
+ if [[ -n "$dep_violations" ]]; then
902
+ while IFS= read -r violation; do
903
+ [[ -z "$violation" ]] && continue
904
+ errors+="$violation\n"
905
+ error_count=$((error_count + 1))
906
+ done <<< "$dep_violations"
907
+ fi
908
+
909
+ # Check file overlap within same batch (create/modify only)
910
+ local file_overlaps
911
+ file_overlaps=$(jq -r '
912
+ . as $root |
913
+ [$root.stories[] | select(.batch != null) | .batch] | unique | .[] as $b |
914
+ [$root.stories[] | select(.batch == $b)] |
915
+ if length < 2 then empty
916
+ else
917
+ . as $stories |
918
+ range(length) as $i | range($i+1; length) as $j |
919
+ $stories[$i] as $a | $stories[$j] as $b_story |
920
+ (($a.files.create // []) + ($a.files.modify // [])) as $files_a |
921
+ (($b_story.files.create // []) + ($b_story.files.modify // [])) as $files_b |
922
+ ($files_a | map(select(. as $f | $files_b | any(. == $f)))) as $shared |
923
+ select(($shared | length) > 0) |
924
+ "batch \($b): \($a.id) and \($b_story.id) share files: \($shared | join(", "))"
925
+ end
926
+ ' "$prd_file" 2>/dev/null)
927
+
928
+ if [[ -n "$file_overlaps" ]]; then
929
+ while IFS= read -r overlap; do
930
+ [[ -z "$overlap" ]] && continue
931
+ errors+="$overlap\n"
932
+ error_count=$((error_count + 1))
933
+ done <<< "$file_overlaps"
934
+ fi
935
+
936
+ if [[ $error_count -gt 0 ]]; then
937
+ echo -e "$errors"
938
+ return 1
939
+ fi
940
+
941
+ return 0
942
+ }
943
+
815
944
  # Ensure database migrations are applied before verification
816
945
  # Migration commands are idempotent - they no-op if nothing pending
817
946
  run_migrations_if_needed() {
@@ -195,7 +195,9 @@ run_unit_tests() {
195
195
  if [[ -f "package.json" ]] && grep -q '"test"' package.json; then
196
196
  test_cmd="npm test"
197
197
  elif [[ -f "pytest.ini" ]] || [[ -f "pyproject.toml" ]]; then
198
- test_cmd="pytest"
198
+ local py_runner
199
+ py_runner=$(detect_python_runner ".")
200
+ test_cmd="${py_runner}${py_runner:+ }pytest"
199
201
  elif [[ -f "Cargo.toml" ]]; then
200
202
  test_cmd="cargo test"
201
203
  elif [[ -f "go.mod" ]]; then
@@ -217,8 +219,35 @@ run_unit_tests() {
217
219
  else
218
220
  print_error "failed"
219
221
  echo ""
220
- echo " Output (last $MAX_LOG_LINES lines):"
221
- tail -"$MAX_LOG_LINES" "$log_file" | sed 's/^/ /'
222
+
223
+ # Check for missing tool (uv, poetry, pytest, etc.)
224
+ if grep -qiE "command not found|no such file or directory" "$log_file" 2>/dev/null; then
225
+ local missing_tool=""
226
+ if grep -qi "uv.*command not found\|uv:.*not found" "$log_file" 2>/dev/null; then
227
+ missing_tool="uv"
228
+ elif grep -qi "poetry.*command not found\|poetry:.*not found" "$log_file" 2>/dev/null; then
229
+ missing_tool="poetry"
230
+ elif grep -qi "pytest.*command not found\|pytest:.*not found" "$log_file" 2>/dev/null; then
231
+ missing_tool="pytest"
232
+ fi
233
+
234
+ if [[ -n "$missing_tool" ]]; then
235
+ echo " '$missing_tool' is not installed."
236
+ echo ""
237
+ echo " Run setup to auto-detect and configure your project tools:"
238
+ echo " npx agentic-loop setup"
239
+ echo ""
240
+ echo " See Step 3 of the Getting Started guide:"
241
+ echo " docs/GETTING-STARTED.md"
242
+ else
243
+ echo " Output (last $MAX_LOG_LINES lines):"
244
+ tail -"$MAX_LOG_LINES" "$log_file" | sed 's/^/ /'
245
+ fi
246
+ else
247
+ echo " Output (last $MAX_LOG_LINES lines):"
248
+ tail -"$MAX_LOG_LINES" "$log_file" | sed 's/^/ /'
249
+ fi
250
+
222
251
  cp "$log_file" "$RALPH_DIR/last_test_failure.log"
223
252
  rm -f "$log_file"
224
253
  return 1
@@ -228,9 +257,9 @@ run_unit_tests() {
228
257
  # Expand config placeholders in a string
229
258
  # Usage: _expand_config_vars "curl {config.urls.backend}/api"
230
259
  # Expands any {config.X.Y} placeholder from .ralph/config.json via jq.
231
- # Known placeholders have fallback paths for backward compatibility:
260
+ # Known placeholders with fallback paths:
232
261
  # {config.urls.backend} -> .urls.backend // .api.baseUrl
233
- # {config.urls.frontend} -> .urls.frontend // .testUrlBase
262
+ # {config.urls.frontend} -> .urls.frontend
234
263
  _expand_config_vars() {
235
264
  local input="$1"
236
265
  local config="$RALPH_DIR/config.json"
@@ -249,7 +278,7 @@ _expand_config_vars() {
249
278
 
250
279
  if [[ "$result" == *"{config.urls.frontend}"* ]]; then
251
280
  local val
252
- val=$(jq -r '.urls.frontend // .testUrlBase // empty' "$config" 2>/dev/null)
281
+ val=$(jq -r '.urls.frontend // empty' "$config" 2>/dev/null)
253
282
  [[ -n "$val" ]] && result="${result//\{config.urls.frontend\}/$val}"
254
283
  fi
255
284
 
@@ -276,6 +305,17 @@ _expand_config_vars() {
276
305
  echo "$result"
277
306
  }
278
307
 
308
+ # Check if a command string contains unresolved auth placeholder variables
309
+ # Returns 0 (true) if auth placeholders found, 1 (false) if clean
310
+ _has_auth_placeholder() {
311
+ local cmd="$1"
312
+ local auth_vars='TOKEN|API_KEY|JWT|AUTH_TOKEN|BEARER_TOKEN|ACCESS_TOKEN|SECRET|PASSWORD|CREDENTIALS|API_SECRET|AUTH'
313
+ [[ "$cmd" =~ \$($auth_vars)[^A-Za-z_] ]] && return 0
314
+ [[ "$cmd" =~ \$($auth_vars)$ ]] && return 0
315
+ [[ "$cmd" =~ \$\{($auth_vars)\} ]] && return 0
316
+ return 1
317
+ }
318
+
279
319
  # Verify PRD acceptance criteria / test steps
280
320
  verify_prd_criteria() {
281
321
  local story="$1"
@@ -296,13 +336,21 @@ verify_prd_criteria() {
296
336
  # Clear previous PRD failure log
297
337
  rm -f "$prd_failure_log"
298
338
 
299
- local step_index=0
339
+ local step_index=-1
300
340
  while IFS= read -r step; do
301
341
  [[ -z "$step" ]] && continue
302
342
 
303
343
  # Expand config placeholders (e.g., {config.urls.backend})
304
344
  local expanded_step
305
345
  expanded_step=$(_expand_config_vars "$step")
346
+ ((step_index++)) || true
347
+
348
+ # Skip steps with unresolved auth placeholders — don't fail, just warn
349
+ if _has_auth_placeholder "$expanded_step"; then
350
+ echo -n " $expanded_step... "
351
+ print_warning "skipped (uses auth placeholder variable — set a real value in env or config)"
352
+ continue
353
+ fi
306
354
 
307
355
  echo -n " $expanded_step... "
308
356
 
@@ -311,8 +359,16 @@ verify_prd_criteria() {
311
359
  else
312
360
  print_error "failed"
313
361
  echo ""
314
- echo " Output:"
315
- tail -"$MAX_OUTPUT_PREVIEW_LINES" "$log_file" | sed 's/^/ /'
362
+
363
+ # Check for connection refused — give actionable guidance
364
+ if grep -qiE "connection refused|couldn.t connect|failed to connect" "$log_file" 2>/dev/null; then
365
+ local dev_cmd
366
+ dev_cmd=$(get_config '.commands.dev' "docker compose up -d")
367
+ echo " Server not running. Start it before running Ralph: $dev_cmd"
368
+ else
369
+ echo " Output:"
370
+ tail -"$MAX_OUTPUT_PREVIEW_LINES" "$log_file" | sed 's/^/ /'
371
+ fi
316
372
 
317
373
  # Save failure details for retry context
318
374
  {
@@ -325,7 +381,6 @@ verify_prd_criteria() {
325
381
 
326
382
  failed=1
327
383
  fi
328
- ((step_index++)) || true
329
384
  done <<< "$test_steps"
330
385
 
331
386
  rm -f "$log_file"
@@ -158,6 +158,8 @@ After completing the story:
158
158
  - No commented-out code
159
159
  - All tests passing
160
160
 
161
+ 4. **Then stop.** Your session should end here. Ralph will automatically run verification (lint, tests, build) and mark the story as passed or failed. You do not need to mark anything — just finish implementing and stop.
162
+
161
163
  ---
162
164
 
163
165
  ## Rules
@@ -166,18 +168,22 @@ After completing the story:
166
168
  2. **Follow the PRD** - It has all the context you need
167
169
  3. **Read before coding** - Understand existing patterns first
168
170
  4. **Test frequently** - Run tests after each significant change
169
- 5. **NEVER edit prd.json** - Ralph handles story completion
171
+ 5. **Don't edit prd.json** - You can read it for context, but Ralph manages story state (passes, retryCount, skipped). After your session ends, Ralph runs verification and marks the story. Editing these fields has no effect — Ralph resets them before verifying.
170
172
  6. **Don't give up** - If verification fails, fix and retry
171
173
 
172
174
  ---
173
175
 
174
176
  ## If Blocked
175
177
 
176
- If you encounter a blocker you cannot resolve:
178
+ If you encounter a blocker you cannot resolve after 2-3 attempts:
179
+
177
180
  1. Document the issue in `.ralph/progress.txt`
178
181
  2. Note what you tried and why it didn't work
179
- 3. Suggest potential solutions
180
- 4. Do NOT mark the story as passing
182
+ 3. Signal Ralph to skip this story:
183
+ ```bash
184
+ echo "BLOCKED: [reason]" > .ralph/.blocked
185
+ ```
186
+ 4. Then stop. Ralph will stop the loop so the issue can be resolved.
181
187
 
182
188
  ---
183
189