agentic-loop 3.8.0 → 3.9.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.
@@ -54,7 +54,7 @@ Help the user flesh out the idea through conversation:
54
54
  **Security (IMPORTANT - ask if feature involves):**
55
55
  - Authentication: Who can access this? Login required?
56
56
  - Passwords: How stored? (must be hashed, never plain text)
57
- - User input: What validation needed? (prevent injection)
57
+ - User input: What validation needed? (SQL injection, XSS, command injection)
58
58
  - Sensitive data: What should NEVER be in API responses?
59
59
  - Rate limiting: Should this be rate limited? (login attempts, API calls)
60
60
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.8.0",
3
+ "version": "3.9.0",
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",
package/ralph/init.sh CHANGED
@@ -496,6 +496,89 @@ auto_configure_project() {
496
496
  done
497
497
  fi
498
498
 
499
+ # 7. Detect test directory and patterns
500
+ local test_dir=""
501
+ local test_patterns=""
502
+
503
+ # Check for common test directories
504
+ for dir in "tests" "test" "__tests__" "spec" \
505
+ "src/__tests__" "src/test" \
506
+ "apps/api/tests" "apps/web/tests" \
507
+ "backend/tests" "frontend/tests"; do
508
+ if [[ -d "$dir" ]]; then
509
+ test_dir="$dir"
510
+ break
511
+ fi
512
+ done
513
+
514
+ # If no directory found, check for colocated test files
515
+ if [[ -z "$test_dir" ]]; then
516
+ # Look for test files anywhere (colocated pattern)
517
+ local test_file=""
518
+ test_file=$(find . -type f \( \
519
+ -name "*_test.py" -o -name "test_*.py" -o \
520
+ -name "*.test.ts" -o -name "*.test.js" -o -name "*.test.tsx" -o \
521
+ -name "*.spec.ts" -o -name "*.spec.js" -o \
522
+ -name "*_test.go" -o -name "*_test.rs" \
523
+ \) -not -path "*/node_modules/*" -not -path "*/.venv/*" -not -path "*/venv/*" \
524
+ -not -path "*/.git/*" -not -path "*/dist/*" -not -path "*/build/*" \
525
+ -not -path "*/__pycache__/*" -not -path "*/coverage/*" 2>/dev/null | head -1 || true)
526
+
527
+ if [[ -n "$test_file" ]]; then
528
+ # Tests are colocated with source (e.g., src/component.test.ts)
529
+ test_dir="src"
530
+ [[ ! -d "src" ]] && test_dir="."
531
+ fi
532
+ fi
533
+
534
+ # Detect test patterns based on project type (combine for mixed projects)
535
+ test_patterns=""
536
+ if [[ -f "pyproject.toml" || -f "requirements.txt" ]]; then
537
+ test_patterns="*_test.py,test_*.py"
538
+ fi
539
+ if [[ -f "package.json" ]]; then
540
+ [[ -n "$test_patterns" ]] && test_patterns+=","
541
+ test_patterns+="*.test.ts,*.test.tsx,*.test.js,*.spec.ts,*.spec.tsx,*.spec.js"
542
+ fi
543
+ if [[ -f "go.mod" ]]; then
544
+ [[ -n "$test_patterns" ]] && test_patterns+=","
545
+ test_patterns+="*_test.go"
546
+ fi
547
+ if [[ -f "Cargo.toml" ]]; then
548
+ [[ -n "$test_patterns" ]] && test_patterns+=","
549
+ test_patterns+="*_test.rs"
550
+ fi
551
+ if [[ -f "mix.exs" ]]; then
552
+ [[ -n "$test_patterns" ]] && test_patterns+=","
553
+ test_patterns+="*_test.exs"
554
+ fi
555
+
556
+ if [[ -n "$test_dir" ]]; then
557
+ if ! jq -e '.tests.directory' "$tmpfile" >/dev/null 2>&1 || [[ "$(jq -r '.tests.directory' "$tmpfile")" == "" ]]; then
558
+ jq --arg dir "$test_dir" --arg patterns "$test_patterns" \
559
+ '.tests.directory = $dir | .tests.patterns = $patterns' \
560
+ "$tmpfile" > "${tmpfile}.new" && mv "${tmpfile}.new" "$tmpfile"
561
+ echo " Auto-detected tests.directory: $test_dir"
562
+ updated=true
563
+ fi
564
+ else
565
+ # No tests found - check if warning is suppressed
566
+ local require_tests
567
+ require_tests=$(jq -r '.checks.requireTests // true' "$tmpfile" 2>/dev/null)
568
+
569
+ if [[ "$require_tests" == "true" ]]; then
570
+ echo ""
571
+ print_warning "No test directory or test files found."
572
+ echo " Without tests, Ralph can only verify syntax and API responses."
573
+ echo " Import errors and integration issues won't be caught."
574
+ echo ""
575
+ echo " To fix: Add tests, or set in .ralph/config.json:"
576
+ echo " {\"tests\": {\"directory\": \"src\", \"patterns\": \"*.test.ts\"}}"
577
+ echo " To silence: {\"checks\": {\"requireTests\": false}}"
578
+ echo ""
579
+ fi
580
+ fi
581
+
499
582
  # Save if updated
500
583
  if [[ "$updated" == "true" ]]; then
501
584
  mv "$tmpfile" "$config"
package/ralph/utils.sh CHANGED
@@ -509,6 +509,27 @@ validate_prd() {
509
509
  print_warning "PRD is missing feature name (will show as 'unnamed')"
510
510
  fi
511
511
 
512
+ # Check if project has tests (from config)
513
+ local config="$RALPH_DIR/config.json"
514
+ if [[ -f "$config" ]]; then
515
+ local require_tests
516
+ require_tests=$(jq -r '.checks.requireTests // true' "$config" 2>/dev/null)
517
+ local test_dir
518
+ test_dir=$(jq -r '.tests.directory // empty' "$config" 2>/dev/null)
519
+
520
+ if [[ "$require_tests" == "true" && -z "$test_dir" ]]; then
521
+ echo ""
522
+ print_warning "No test directory configured in .ralph/config.json"
523
+ echo " Without tests, Ralph can only verify syntax and API responses."
524
+ echo " Import errors and integration issues won't be caught."
525
+ echo ""
526
+ echo " To fix: Add tests, or set in .ralph/config.json:"
527
+ echo " {\"tests\": {\"directory\": \"src\", \"patterns\": \"*.test.ts\"}}"
528
+ echo " To silence: {\"checks\": {\"requireTests\": false}}"
529
+ echo ""
530
+ fi
531
+ fi
532
+
512
533
  # Validate and fix individual stories
513
534
  validate_and_fix_stories "$prd_file" || return 1
514
535
 
@@ -589,7 +610,8 @@ validate_and_fix_stories() {
589
610
  fi
590
611
 
591
612
  # Check 5: List endpoints need scale criteria
592
- if echo "$story_title" | grep -qiE "(list|get all|fetch all|index|search)"; then
613
+ # Note: "search" excluded - search endpoints often return single/filtered results
614
+ if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
593
615
  local criteria
594
616
  criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
595
617
  if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
@@ -656,36 +678,51 @@ RULES FOR FIXING:
656
678
  CURRENT PRD:
657
679
  $(cat "$prd_file")
658
680
 
659
- Output ONLY the fixed JSON, no explanation."
681
+ Output ONLY the fixed JSON, no explanation. Start with { and end with }."
660
682
 
683
+ local raw_response
684
+ raw_response=$(echo "$fix_prompt" | run_with_timeout "$CODE_REVIEW_TIMEOUT_SECONDS" claude -p 2>/dev/null)
685
+
686
+ # Extract JSON from response (Claude sometimes adds text before/after)
661
687
  local fixed_prd
662
- fixed_prd=$(echo "$fix_prompt" | claude -p 2>/dev/null)
688
+ fixed_prd=$(echo "$raw_response" | sed -n '/^[[:space:]]*{/,/^[[:space:]]*}[[:space:]]*$/p' | head -1000)
689
+
690
+ # If sed extraction failed, try the raw response
691
+ if [[ -z "$fixed_prd" ]]; then
692
+ fixed_prd="$raw_response"
693
+ fi
663
694
 
664
- # Validate the response is valid JSON
665
- if echo "$fixed_prd" | jq -e . >/dev/null 2>&1; then
666
- # Backup original
667
- cp "$prd_file" "${prd_file}.bak"
695
+ # Validate the response is valid JSON with required structure
696
+ if echo "$fixed_prd" | jq -e '.stories' >/dev/null 2>&1; then
697
+ # Timestamped backup (preserves history across multiple fixes)
698
+ local backup_file="${prd_file}.$(date +%Y%m%d-%H%M%S).bak"
699
+ cp "$prd_file" "$backup_file"
668
700
 
669
701
  # Write fixed PRD
670
702
  echo "$fixed_prd" > "$prd_file"
671
- print_success "PRD auto-fixed (backup at ${prd_file}.bak)"
703
+ print_success "PRD auto-fixed (backup at $backup_file)"
672
704
 
673
705
  # Re-validate to confirm fixes
674
706
  echo " Re-validating..."
675
707
  local remaining_issues
676
708
  remaining_issues=$(validate_stories_quick "$prd_file")
677
709
  if [[ -n "$remaining_issues" ]]; then
678
- print_warning "Some issues remain - may need manual fixes"
710
+ print_warning "Some issues remain - may need manual fixes:"
711
+ echo "$remaining_issues" | tr ',' '\n' | while IFS= read -r line; do
712
+ [[ -n "$line" ]] && echo " $line"
713
+ done
679
714
  else
680
715
  print_success "All issues resolved"
681
716
  fi
682
717
  else
683
718
  print_error "Claude returned invalid JSON - fix manually"
719
+ echo " Response preview: $(echo "$raw_response" | head -3)"
684
720
  return 1
685
721
  fi
686
722
  }
687
723
 
688
724
  # Quick validation without auto-fix (for re-checking after fix)
725
+ # Checks all the same things as validate_and_fix_stories() but returns issues string
689
726
  validate_stories_quick() {
690
727
  local prd_file="$1"
691
728
  local issues=""
@@ -698,17 +735,55 @@ validate_stories_quick() {
698
735
 
699
736
  local story_type
700
737
  story_type=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .type // "unknown"' "$prd_file")
738
+ local story_title
739
+ story_title=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .title // ""' "$prd_file")
701
740
  local test_steps
702
741
  test_steps=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testSteps // [] | join(" ")' "$prd_file")
703
742
 
743
+ # Check 1: testSteps quality
704
744
  if [[ "$story_type" == "backend" ]] && ! echo "$test_steps" | grep -q "curl "; then
705
- issues+="$story_id: still missing curl tests, "
745
+ issues+="$story_id: missing curl tests, "
746
+ fi
747
+ if [[ "$story_type" == "frontend" ]] && ! echo "$test_steps" | grep -qE "(tsc --noEmit|playwright)"; then
748
+ issues+="$story_id: missing tsc/playwright tests, "
706
749
  fi
707
750
 
751
+ # Check 2: Backend needs apiContract
752
+ if [[ "$story_type" == "backend" ]]; then
753
+ local has_contract
754
+ has_contract=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .apiContract // empty' "$prd_file")
755
+ if [[ -z "$has_contract" || "$has_contract" == "null" ]]; then
756
+ issues+="$story_id: missing apiContract, "
757
+ fi
758
+ fi
759
+
760
+ # Check 3: Frontend needs testUrl and contextFiles
708
761
  if [[ "$story_type" == "frontend" ]]; then
709
762
  local has_url
710
763
  has_url=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .testUrl // empty' "$prd_file")
711
- [[ -z "$has_url" ]] && issues+="$story_id: still missing testUrl, "
764
+ [[ -z "$has_url" || "$has_url" == "null" ]] && issues+="$story_id: missing testUrl, "
765
+
766
+ local context_files
767
+ context_files=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .contextFiles // [] | length' "$prd_file")
768
+ [[ "$context_files" == "0" ]] && issues+="$story_id: missing contextFiles, "
769
+ fi
770
+
771
+ # Check 4: Auth stories need security criteria
772
+ if echo "$story_title" | grep -qiE "(login|auth|password|register|signup|sign.?up)"; then
773
+ local criteria
774
+ criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
775
+ if ! echo "$criteria" | grep -qiE "(hash|bcrypt|sanitiz|inject|rate.?limit)"; then
776
+ issues+="$story_id: missing security criteria, "
777
+ fi
778
+ fi
779
+
780
+ # Check 5: List endpoints need scale criteria
781
+ if echo "$story_title" | grep -qiE "(list|get all|fetch all|index)"; then
782
+ local criteria
783
+ criteria=$(jq -r --arg id "$story_id" '.stories[] | select(.id==$id) | .acceptanceCriteria // [] | join(" ")' "$prd_file")
784
+ if ! echo "$criteria" | grep -qiE "(pagina|limit|page=|per.?page)"; then
785
+ issues+="$story_id: missing pagination criteria, "
786
+ fi
712
787
  fi
713
788
  done <<< "$story_ids"
714
789
 
@@ -39,9 +39,15 @@
39
39
  "typecheck": false,
40
40
  "build": true,
41
41
  "test": true,
42
+ "requireTests": true,
42
43
  "fastapi": false
43
44
  },
44
45
 
46
+ "tests": {
47
+ "directory": "test",
48
+ "patterns": "*_test.exs"
49
+ },
50
+
45
51
  "api": {
46
52
  "baseUrl": "http://localhost:4000",
47
53
  "healthEndpoint": "/api/health",
@@ -40,9 +40,15 @@
40
40
  "typecheck": true,
41
41
  "build": false,
42
42
  "test": true,
43
+ "requireTests": true,
43
44
  "fastmcp": true
44
45
  },
45
46
 
47
+ "tests": {
48
+ "directory": "tests",
49
+ "patterns": "*_test.py,test_*.py"
50
+ },
51
+
46
52
  "api": {
47
53
  "baseUrl": "http://localhost:8000",
48
54
  "healthEndpoint": "/health",
@@ -41,9 +41,15 @@
41
41
  "typecheck": true,
42
42
  "build": "final",
43
43
  "test": true,
44
+ "requireTests": true,
44
45
  "fastapi": true
45
46
  },
46
47
 
48
+ "tests": {
49
+ "directory": "tests",
50
+ "patterns": "*.test.ts,*.test.tsx,*_test.py,test_*.py"
51
+ },
52
+
47
53
  "api": {
48
54
  "baseUrl": "http://localhost:8000",
49
55
  "healthEndpoint": "/api/health",
@@ -39,9 +39,15 @@
39
39
  "typecheck": false,
40
40
  "build": true,
41
41
  "test": true,
42
+ "requireTests": true,
42
43
  "fastapi": false
43
44
  },
44
45
 
46
+ "tests": {
47
+ "directory": ".",
48
+ "patterns": "*_test.go"
49
+ },
50
+
45
51
  "api": {
46
52
  "baseUrl": "http://localhost:8080",
47
53
  "healthEndpoint": "/health",
@@ -39,9 +39,15 @@
39
39
  "typecheck": true,
40
40
  "build": "final",
41
41
  "test": true,
42
+ "requireTests": false,
42
43
  "fastapi": false
43
44
  },
44
45
 
46
+ "tests": {
47
+ "directory": "",
48
+ "patterns": ""
49
+ },
50
+
45
51
  "api": {
46
52
  "baseUrl": "http://localhost:3000",
47
53
  "healthEndpoint": "/health",
@@ -39,9 +39,15 @@
39
39
  "typecheck": true,
40
40
  "build": "final",
41
41
  "test": true,
42
+ "requireTests": true,
42
43
  "fastapi": false
43
44
  },
44
45
 
46
+ "tests": {
47
+ "directory": "tests",
48
+ "patterns": "*.test.ts,*.test.tsx,*.test.js,*.spec.ts,*.spec.tsx,*.spec.js"
49
+ },
50
+
45
51
  "api": {
46
52
  "baseUrl": "http://localhost:3000",
47
53
  "healthEndpoint": "/api/health",
@@ -39,9 +39,15 @@
39
39
  "typecheck": false,
40
40
  "build": false,
41
41
  "test": true,
42
+ "requireTests": true,
42
43
  "fastapi": true
43
44
  },
44
45
 
46
+ "tests": {
47
+ "directory": "tests",
48
+ "patterns": "*_test.py,test_*.py"
49
+ },
50
+
45
51
  "api": {
46
52
  "baseUrl": "http://localhost:8000",
47
53
  "healthEndpoint": "/api/health",
@@ -39,9 +39,15 @@
39
39
  "typecheck": false,
40
40
  "build": true,
41
41
  "test": true,
42
+ "requireTests": true,
42
43
  "fastapi": false
43
44
  },
44
45
 
46
+ "tests": {
47
+ "directory": "tests",
48
+ "patterns": "*_test.rs"
49
+ },
50
+
45
51
  "api": {
46
52
  "baseUrl": "http://localhost:8080",
47
53
  "healthEndpoint": "/health",