agentic-loop 3.18.2 → 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 +35 -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 +60 -82
  32. package/ralph/loop.sh +533 -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
@@ -197,31 +197,19 @@ validate_prd() {
197
197
  fi
198
198
  fi
199
199
 
200
- # Deprecation warnings for old root-level fields
200
+ # Auto-remove deprecated root-level fields (no longer used, safe to strip)
201
201
  local deprecated_fields=""
202
- if jq -e '.techStack' "$prd_file" >/dev/null 2>&1; then
203
- deprecated_fields+="techStack "
204
- fi
205
- if jq -e '.globalConstraints' "$prd_file" >/dev/null 2>&1; then
206
- deprecated_fields+="globalConstraints "
207
- fi
208
- if jq -e '.originalContext' "$prd_file" >/dev/null 2>&1; then
209
- deprecated_fields+="originalContext "
210
- fi
211
- if jq -e '.testing' "$prd_file" >/dev/null 2>&1; then
212
- deprecated_fields+="testing "
213
- fi
214
- if jq -e '.architecture' "$prd_file" >/dev/null 2>&1; then
215
- deprecated_fields+="architecture "
216
- fi
217
- if jq -e '.testUsers' "$prd_file" >/dev/null 2>&1; then
218
- deprecated_fields+="testUsers "
219
- fi
202
+ local deprecated_keys=("techStack" "globalConstraints" "originalContext" "testing" "architecture" "testUsers")
203
+ for key in "${deprecated_keys[@]}"; do
204
+ if jq -e ".$key" "$prd_file" >/dev/null 2>&1; then
205
+ deprecated_fields+="$key "
206
+ fi
207
+ done
220
208
  if [[ -n "$deprecated_fields" ]]; then
221
209
  echo ""
222
- print_warning "Found deprecated root-level fields: $deprecated_fields"
223
- echo " These should be in each story instead. Regenerate with /prd."
224
- echo ""
210
+ print_info "Removing deprecated root-level fields: $deprecated_fields"
211
+ update_json "$prd_file" \
212
+ 'del(.techStack, .globalConstraints, .originalContext, .testing, .architecture, .testUsers)'
225
213
  fi
226
214
 
227
215
  # Validate API smoke test configuration in background (skip in fast/cached mode)
@@ -233,11 +221,23 @@ validate_prd() {
233
221
  api_check_pid=$!
234
222
  fi
235
223
 
224
+ # Validate batch assignments (warn, don't block)
225
+ local batch_errors batch_rc=0
226
+ batch_errors=$(validate_batch_assignments "$prd_file" 2>/dev/null) || batch_rc=$?
227
+ if [[ $batch_rc -ne 0 && -n "$batch_errors" ]]; then
228
+ echo ""
229
+ print_warning "Batch assignment issues:"
230
+ echo "$batch_errors" | while IFS= read -r line; do
231
+ [[ -n "$line" ]] && echo " $line"
232
+ done
233
+ echo ""
234
+ fi
235
+
236
236
  # Replace hardcoded paths with config placeholders
237
237
  fix_hardcoded_paths "$prd_file" "$config"
238
238
 
239
239
  # Validate and fix individual stories
240
- # dry_run flag — when "true", skip auto-fix
240
+ # dry_run flag — when "true", report issues but skip auto-fix
241
241
  _validate_and_fix_stories "$prd_file" "$dry_run" || return 1
242
242
 
243
243
  # Wait for background API health check and print its output
@@ -326,6 +326,107 @@ _validate_api_config() {
326
326
  return 0
327
327
  }
328
328
 
329
+ # Check a single story for common issues
330
+ # Outputs issue strings to stdout (one per line: "issue description")
331
+ # $1: story_id $2: prd_file
332
+ _check_story_issues() {
333
+ local story_id="$1"
334
+ local prd_file="$2"
335
+
336
+ local story_type
337
+ story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
338
+ local story_title
339
+ story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
340
+ local test_steps
341
+ test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
342
+
343
+ # Backend must have curl tests
344
+ if [[ "$story_type" == "backend" ]] && [[ -n "$test_steps" ]] && ! echo "$test_steps" | grep -q "curl "; then
345
+ echo "backend needs curl tests"
346
+ fi
347
+
348
+ # Frontend must have tsc or playwright
349
+ if [[ "$story_type" == "frontend" ]] && [[ -n "$test_steps" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
350
+ echo "frontend needs tsc/playwright tests"
351
+ fi
352
+
353
+ # Backend needs apiContract
354
+ if [[ "$story_type" == "backend" ]]; then
355
+ local has_contract
356
+ has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
357
+ if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
358
+ echo "missing apiContract"
359
+ fi
360
+ fi
361
+
362
+ # Frontend needs testUrl, contextFiles, and mcp
363
+ if [[ "$story_type" == "frontend" ]]; then
364
+ local has_url
365
+ has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
366
+ [[ -z "$has_url" || "$has_url" == "null" ]] && echo "missing testUrl"
367
+
368
+ local context_files
369
+ context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
370
+ [[ "$context_files" == "0" ]] && echo "missing contextFiles"
371
+
372
+ local mcp_tools
373
+ mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
374
+ [[ "$mcp_tools" == "0" ]] && echo "missing mcp (browser tools)"
375
+ fi
376
+
377
+ # Auth stories need security criteria
378
+ if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
379
+ local criteria
380
+ criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
381
+ if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
382
+ echo "missing security criteria"
383
+ fi
384
+ fi
385
+
386
+ # List endpoints need pagination criteria
387
+ if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
388
+ local criteria
389
+ criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
390
+ if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
391
+ echo "missing pagination criteria"
392
+ fi
393
+ fi
394
+
395
+ # API consumer needs camelCase transformation note
396
+ if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
397
+ local story_desc
398
+ story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
399
+ if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
400
+ local story_notes
401
+ story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
402
+ if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
403
+ echo "API consumer needs camelCase transformation note"
404
+ fi
405
+ fi
406
+ fi
407
+
408
+ # All testSteps are server-dependent
409
+ if [[ -n "$test_steps" ]]; then
410
+ local has_offline=false has_server=false
411
+ local step_list
412
+ step_list=$(jq -r --arg id "$story_id" \
413
+ '.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
414
+
415
+ while IFS= read -r single_step; do
416
+ [[ -z "$single_step" ]] && continue
417
+ if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
418
+ has_server=true
419
+ else
420
+ has_offline=true
421
+ fi
422
+ done <<< "$step_list"
423
+
424
+ if [[ "$has_server" == "true" && "$has_offline" == "false" ]]; then
425
+ echo "all testSteps need a live server (add offline test: npm test, tsc --noEmit, pytest)"
426
+ fi
427
+ fi
428
+ }
429
+
329
430
  # Validate individual stories and auto-fix with Claude if needed
330
431
  # $1: prd_file $2: optional "dry_run" — when "true", report issues but skip auto-fix
331
432
  _validate_and_fix_stories() {
@@ -339,7 +440,7 @@ _validate_and_fix_stories() {
339
440
  local cnt_no_tests=0 cnt_backend_curl=0 cnt_backend_contract=0
340
441
  local cnt_frontend_tsc=0 cnt_frontend_url=0 cnt_frontend_context=0 cnt_frontend_mcp=0
341
442
  local cnt_auth_security=0 cnt_list_pagination=0 cnt_prose_steps=0
342
- local cnt_naming_convention=0 cnt_bare_pytest=0
443
+ local cnt_naming_convention=0 cnt_bare_pytest=0 cnt_bare_python=0
343
444
  local cnt_server_only=0
344
445
  local cnt_custom=0
345
446
 
@@ -356,156 +457,64 @@ _validate_and_fix_stories() {
356
457
  [[ -z "$story_id" ]] && continue
357
458
 
358
459
  local story_issues=""
359
- local story_type
360
- story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
361
- local story_title
362
- story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
363
-
364
- # Check 1: testSteps quality
365
460
  local test_steps
366
461
  test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
367
462
 
463
+ # Checks unique to full validation: empty testSteps, prose detection, bare pytest
368
464
  if [[ -z "$test_steps" ]]; then
369
465
  story_issues+="no testSteps, "
370
466
  cnt_no_tests=$((cnt_no_tests + 1))
371
467
  else
372
- # Check test steps are executable commands, not prose
373
- # Good: "curl -s POST /api/login | jq -e '.token'"
374
- # Bad: "Verify the user can log in successfully"
375
468
  if ! echo "$test_steps" | grep -qE "(curl |npm |pytest|go test|cargo test|mix test|rails test|bundle exec|python |node |sh |bash |\| jq)"; then
376
469
  story_issues+="testSteps look like prose (need executable commands), "
377
470
  cnt_prose_steps=$((cnt_prose_steps + 1))
378
471
  fi
379
472
 
380
- # Type-specific checks
381
- if [[ "$story_type" == "backend" ]]; then
382
- # Backend must have curl, not just npm test/pytest
383
- if ! echo "$test_steps" | grep -q "curl "; then
384
- story_issues+="backend needs curl tests, "
385
- cnt_backend_curl=$((cnt_backend_curl + 1))
386
- fi
387
- elif [[ "$story_type" == "frontend" ]]; then
388
- # Frontend must have tsc or playwright
389
- if ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
390
- story_issues+="frontend needs tsc/playwright tests, "
391
- cnt_frontend_tsc=$((cnt_frontend_tsc + 1))
392
- fi
393
- fi
394
-
395
- # Check for bare pytest/python in projects using uv/poetry/pipenv
396
473
  local py_runner
397
474
  py_runner=$(detect_python_runner ".")
398
475
  if [[ -n "$py_runner" ]]; then
399
- # Project uses a Python runner - check for bare pytest/python commands
400
- # Match: "pytest " at start or after space/semicolon, but not preceded by "run "
401
476
  if echo "$test_steps" | grep -qE '(^|[; ])pytest ' && ! echo "$test_steps" | grep -qE "(uv run|poetry run|pipenv run) pytest"; then
402
477
  story_issues+="use '$py_runner pytest' not bare 'pytest', "
403
478
  cnt_bare_pytest=$((cnt_bare_pytest + 1))
404
479
  fi
405
480
  fi
406
- fi
407
-
408
- # Check 2: Backend needs apiContract
409
- if [[ "$story_type" == "backend" ]]; then
410
- local has_contract
411
- has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
412
- if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
413
- story_issues+="missing apiContract, "
414
- cnt_backend_contract=$((cnt_backend_contract + 1))
415
- fi
416
- fi
417
-
418
- # Check 3: Frontend needs testUrl, contextFiles, and mcp
419
- if [[ "$story_type" == "frontend" ]]; then
420
- local has_url
421
- has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
422
- if [[ -z "$has_url" || "$has_url" == "null" ]]; then
423
- story_issues+="missing testUrl, "
424
- cnt_frontend_url=$((cnt_frontend_url + 1))
425
- fi
426
-
427
- local context_files
428
- context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
429
- if [[ "$context_files" == "0" ]]; then
430
- story_issues+="missing contextFiles, "
431
- cnt_frontend_context=$((cnt_frontend_context + 1))
432
- fi
433
481
 
434
- # Frontend must have mcp tools for browser verification
435
- local mcp_tools
436
- mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
437
- if [[ "$mcp_tools" == "0" ]]; then
438
- story_issues+="missing mcp (browser tools), "
439
- cnt_frontend_mcp=$((cnt_frontend_mcp + 1))
440
- fi
441
- fi
442
-
443
- # Check 4: Auth stories need security criteria
444
- if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
445
- local criteria
446
- criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
447
- if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
448
- story_issues+="missing security criteria, "
449
- cnt_auth_security=$((cnt_auth_security + 1))
450
- fi
451
- fi
452
-
453
- # Check 5: List endpoints need scale criteria
454
- # Note: "search" excluded - search endpoints often return single/filtered results
455
- if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
456
- local criteria
457
- criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
458
- if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
459
- story_issues+="missing pagination criteria, "
460
- cnt_list_pagination=$((cnt_list_pagination + 1))
461
- fi
462
- fi
463
-
464
- # Check 7: Frontend stories consuming APIs need naming convention notes
465
- # If story is frontend/general AND mentions API/fetch/axios, ensure notes include camelCase guidance
466
- if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
467
- local story_desc
468
- story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
469
- if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
470
- local story_notes
471
- story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
472
- if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
473
- story_issues+="API consumer needs camelCase transformation note, "
474
- cnt_naming_convention=$((cnt_naming_convention + 1))
475
- fi
476
- fi
477
- fi
478
-
479
- # Check 9: Stories where ALL testSteps depend on a live server
480
- # If every testStep is a curl/wget/httpie command and none are offline
481
- # (npm test, pytest, tsc, playwright, cargo test, go test, etc.),
482
- # the story will always fail without a running server.
483
- if [[ -n "$test_steps" ]]; then
484
- local has_offline_step=false
485
- local has_server_step=false
486
- local step_list
487
- step_list=$(jq -r --arg id "$story_id" \
488
- '.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
489
-
490
- while IFS= read -r single_step; do
491
- [[ -z "$single_step" ]] && continue
492
- if echo "$single_step" | grep -qE "^(curl |wget |http )"; then
493
- has_server_step=true
482
+ # Check for bare 'python' (fails on macOS which only has python3)
483
+ if echo "$test_steps" | grep -qE '(^|[;&| ])python ' && ! echo "$test_steps" | grep -qE "(uv run|poetry run|pipenv run|python3) "; then
484
+ if [[ -n "$py_runner" ]]; then
485
+ story_issues+="use '$py_runner python' not bare 'python', "
494
486
  else
495
- has_offline_step=true
487
+ story_issues+="use 'python3' not bare 'python' (macOS has no 'python'), "
496
488
  fi
497
- done <<< "$step_list"
498
-
499
- if [[ "$has_server_step" == "true" && "$has_offline_step" == "false" ]]; then
500
- story_issues+="all testSteps need a live server (add offline test: npm test, tsc --noEmit, pytest), "
501
- cnt_server_only=$((cnt_server_only + 1))
489
+ cnt_bare_python=$((cnt_bare_python + 1))
502
490
  fi
503
491
  fi
504
492
 
493
+ # Shared checks (same as validate_stories_quick)
494
+ local shared_issues
495
+ shared_issues=$(_check_story_issues "$story_id" "$prd_file")
496
+ while IFS= read -r issue; do
497
+ [[ -z "$issue" ]] && continue
498
+ story_issues+="$issue, "
499
+ # Count by category for summary display
500
+ case "$issue" in
501
+ "backend needs curl tests") cnt_backend_curl=$((cnt_backend_curl + 1)) ;;
502
+ "frontend needs tsc/playwright tests") cnt_frontend_tsc=$((cnt_frontend_tsc + 1)) ;;
503
+ "missing apiContract") cnt_backend_contract=$((cnt_backend_contract + 1)) ;;
504
+ "missing testUrl") cnt_frontend_url=$((cnt_frontend_url + 1)) ;;
505
+ "missing contextFiles") cnt_frontend_context=$((cnt_frontend_context + 1)) ;;
506
+ "missing mcp (browser tools)") cnt_frontend_mcp=$((cnt_frontend_mcp + 1)) ;;
507
+ "missing security criteria") cnt_auth_security=$((cnt_auth_security + 1)) ;;
508
+ "missing pagination criteria") cnt_list_pagination=$((cnt_list_pagination + 1)) ;;
509
+ "API consumer needs camelCase transformation note") cnt_naming_convention=$((cnt_naming_convention + 1)) ;;
510
+ "all testSteps need a live server"*) cnt_server_only=$((cnt_server_only + 1)) ;;
511
+ esac
512
+ done <<< "$shared_issues"
513
+
505
514
  # Snapshot built-in issues before custom checks append
506
515
  local builtin_story_issues="$story_issues"
507
516
 
508
- # Check 8: User-defined custom checks (.ralph/checks/prd/)
517
+ # User-defined custom checks (.ralph/checks/prd/)
509
518
  if [[ -d ".ralph/checks/prd" ]]; then
510
519
  local story_json
511
520
  story_json=$(jq --arg id "$story_id" '.stories[] | select(.id==$id)' "$prd_file")
@@ -546,7 +555,8 @@ _validate_and_fix_stories() {
546
555
  [[ $cnt_auth_security -gt 0 ]] && echo " ${cnt_auth_security}x auth: add security criteria"
547
556
  [[ $cnt_list_pagination -gt 0 ]] && echo " ${cnt_list_pagination}x list: add pagination"
548
557
  [[ $cnt_naming_convention -gt 0 ]] && echo " ${cnt_naming_convention}x API consumer: add camelCase transformation note"
549
- [[ $cnt_bare_pytest -gt 0 ]] && echo " ${cnt_bare_pytest}x use 'uv run pytest' not bare 'pytest'"
558
+ [[ $cnt_bare_pytest -gt 0 ]] && echo " ${cnt_bare_pytest}x use '${py_runner:-python3} pytest' not bare 'pytest'"
559
+ [[ $cnt_bare_python -gt 0 ]] && echo " ${cnt_bare_python}x use 'python3' not bare 'python' (macOS compatibility)"
550
560
  [[ $cnt_server_only -gt 0 ]] && echo " ${cnt_server_only}x all testSteps need live server (add offline fallback)"
551
561
  [[ $cnt_custom -gt 0 ]] && echo " ${cnt_custom} stories with custom check issues"
552
562
 
@@ -662,8 +672,8 @@ _group_issues_by_story() {
662
672
  }
663
673
 
664
674
  # Apply instant mechanical fixes using jq (no LLM needed)
665
- # Fixes: missing mcp, bare pytest, missing camelCase note, missing migration prerequisites,
666
- # server-only testSteps
675
+ # Fixes: missing mcp, bare pytest, bare python (macOS compat), missing camelCase note,
676
+ # missing migration prerequisites, server-only testSteps
667
677
  _apply_mechanical_fixes() {
668
678
  local prd_file="$1"
669
679
  local fixed=0
@@ -701,6 +711,19 @@ _apply_mechanical_fixes() {
701
711
  fi
702
712
  fi
703
713
 
714
+ # Fix: Bare python → prefix with runner or replace with python3 (macOS compat)
715
+ local test_steps_for_python
716
+ test_steps_for_python=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join("\n")' "$prd_file")
717
+ if echo "$test_steps_for_python" | grep -qE '(^|[;&| ])python ' && ! echo "$test_steps_for_python" | grep -qE "(uv run|poetry run|pipenv run|python3) "; then
718
+ if [[ -n "$py_runner" ]]; then
719
+ update_json "$prd_file" --arg id "$story_id" --arg runner "$py_runner" \
720
+ '(.stories[] | select(.id==$id) | .testSteps) |= [.[]? | gsub("(?<pre>^|[;&| ])python "; "\(.pre)\($runner) python ")]' && fixed=$((fixed + 1))
721
+ else
722
+ update_json "$prd_file" --arg id "$story_id" \
723
+ '(.stories[] | select(.id==$id) | .testSteps) |= [.[]? | gsub("(?<pre>^|[;&| ])python "; "\(.pre)python3 ")]' && fixed=$((fixed + 1))
724
+ fi
725
+ fi
726
+
704
727
  # Fix: Frontend/general API consumer missing camelCase note
705
728
  if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
706
729
  local story_desc
@@ -910,100 +933,18 @@ validate_stories_quick() {
910
933
  local prd_file="$1"
911
934
  local issues=""
912
935
 
913
- # Only check incomplete stories
914
936
  local story_ids
915
937
  story_ids=$(jq -r '.stories[] | select(.passes != true) | .id' "$prd_file" 2>/dev/null)
916
938
 
917
939
  while IFS= read -r story_id; do
918
940
  [[ -z "$story_id" ]] && continue
919
941
 
920
- local story_type
921
- story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
922
- local story_title
923
- story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
924
- local test_steps
925
- test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
926
-
927
- # Check 1: testSteps quality
928
- if [[ "$story_type" == "backend" ]] && ! echo "$test_steps" | grep -q "curl "; then
929
- issues+="$story_id: missing curl tests, "
930
- fi
931
- if [[ "$story_type" == "frontend" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
932
- issues+="$story_id: missing tsc/playwright tests, "
933
- fi
934
-
935
- # Check 2: Backend needs apiContract
936
- if [[ "$story_type" == "backend" ]]; then
937
- local has_contract
938
- has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
939
- if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
940
- issues+="$story_id: missing apiContract, "
941
- fi
942
- fi
943
-
944
- # Check 3: Frontend needs testUrl, contextFiles, and mcp
945
- if [[ "$story_type" == "frontend" ]]; then
946
- local has_url
947
- has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
948
- [[ -z "$has_url" || "$has_url" == "null" ]] && issues+="$story_id: missing testUrl, "
949
-
950
- local context_files
951
- context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
952
- [[ "$context_files" == "0" ]] && issues+="$story_id: missing contextFiles, "
953
-
954
- local mcp_tools
955
- mcp_tools=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .mcp // [] | length' "$prd_file")
956
- [[ "$mcp_tools" == "0" ]] && issues+="$story_id: missing mcp, "
957
- fi
958
-
959
- # Check 4: Auth stories need security criteria
960
- if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
961
- local criteria
962
- criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
963
- if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
964
- issues+="$story_id: missing security criteria, "
965
- fi
966
- fi
967
-
968
- # Check 5: List endpoints need scale criteria
969
- if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
970
- local criteria
971
- criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
972
- if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
973
- issues+="$story_id: missing pagination criteria, "
974
- fi
975
- fi
976
-
977
- # Check 7: Frontend/general stories consuming APIs need naming convention notes
978
- if [[ "$story_type" == "frontend" || "$story_type" == "general" ]]; then
979
- local story_desc
980
- story_desc=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | (.title + " " + (.acceptanceCriteria // [] | join(" ")) + " " + (.notes // ""))' "$prd_file")
981
- if echo "$story_desc" | grep -qiE "(api|fetch|axios|endpoint|backend|response)"; then
982
- local story_notes
983
- story_notes=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .notes // ""' "$prd_file")
984
- if ! echo "$story_notes" | grep -qiE "(camelCase|snake_case|naming)"; then
985
- issues+="$story_id: needs camelCase transformation note, "
986
- fi
987
- fi
988
- fi
989
-
990
- # Check 8: All testSteps are server-dependent
991
- if [[ -n "$test_steps" ]]; then
992
- local has_offline=false has_server=false
993
- local steps
994
- steps=$(jq -r --arg id "$story_id" \
995
- '.stories[] | select(.id==$id) | .testSteps[]?' "$prd_file")
996
- while IFS= read -r s; do
997
- [[ -z "$s" ]] && continue
998
- if echo "$s" | grep -qE "^(curl |wget |http )"; then
999
- has_server=true
1000
- else
1001
- has_offline=true
1002
- fi
1003
- done <<< "$steps"
1004
- [[ "$has_server" == "true" && "$has_offline" == "false" ]] && \
1005
- issues+="$story_id: all testSteps need live server, "
1006
- fi
942
+ local story_issues
943
+ story_issues=$(_check_story_issues "$story_id" "$prd_file")
944
+ while IFS= read -r issue; do
945
+ [[ -z "$issue" ]] && continue
946
+ issues+="$story_id: $issue, "
947
+ done <<< "$story_issues"
1007
948
  done <<< "$story_ids"
1008
949
 
1009
950
  echo "$issues"
package/ralph/prd.sh CHANGED
@@ -69,9 +69,10 @@ ralph_prd() {
69
69
  echo "Claude will ask clarifying questions to refine the PRD."
70
70
  echo ""
71
71
 
72
- # Get testUrlBase from config (default to localhost:3000)
72
+ # Get frontend URL from config
73
73
  local test_url_base
74
- test_url_base=$(get_config "testUrlBase" "http://localhost:3000")
74
+ test_url_base=$(get_config '.urls.frontend' "")
75
+ [[ -z "$test_url_base" ]] && test_url_base=$(get_config '.playwright.baseUrl' "http://localhost:3000")
75
76
 
76
77
  # Create the PRD generation prompt using printf to avoid heredoc issues
77
78
  local prompt_file
@@ -130,6 +131,8 @@ ralph_prd() {
130
131
  printf '%s\n' "GOOD: cd frontend && npx tsc --noEmit" >> "$prompt_file"
131
132
  printf '%s\n' "GOOD: npx playwright test tests/e2e/dashboard.spec.ts" >> "$prompt_file"
132
133
  printf '%s\n' "GOOD: npx playwright test --grep 'login flow'" >> "$prompt_file"
134
+ printf '%s\n' "GOOD: python3 -m pytest tests/ (use python3, NOT python — macOS has no 'python')" >> "$prompt_file"
135
+ printf '%s\n' "GOOD: uv run pytest tests/ (when project uses uv)" >> "$prompt_file"
133
136
  printf '\n%s\n' "For UI/visual checks, use Playwright tests that can verify:" >> "$prompt_file"
134
137
  printf '%s\n' "- Element visibility and positioning" >> "$prompt_file"
135
138
  printf '%s\n' "- Console errors (no errors in DevTools)" >> "$prompt_file"
@@ -96,22 +96,8 @@ show_completion_with_tour() {
96
96
  echo ""
97
97
  }
98
98
 
99
- detect_project_type() {
100
- # Django + React (AllThrive pattern)
101
- [[ -d "frontend" && -d "core" ]] && echo "django-react" && return
102
- # Django alone
103
- [[ -f "manage.py" ]] && echo "django" && return
104
- # Rust
105
- [[ -f "Cargo.toml" ]] && echo "rust" && return
106
- # Go
107
- [[ -f "go.mod" ]] && echo "go" && return
108
- # Python
109
- [[ -f "pyproject.toml" || -f "requirements.txt" ]] && echo "python" && return
110
- # Node/TypeScript
111
- [[ -f "package.json" ]] && echo "node" && return
112
- # Minimal/unknown
113
- echo "minimal"
114
- }
99
+ # Source shared detect_project_type from utils.sh
100
+ source "$VIBE_ROOT/ralph/utils.sh"
115
101
 
116
102
  install_claude_skills() {
117
103
  echo -e " ${CYAN}Installing Claude skills...${NC}"