agentic-loop 3.8.0 → 3.9.1

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
 
@@ -157,7 +157,55 @@ For fullstack projects with separate frontend:
157
157
  jq '.commands.dev = "cd frontend && npm run dev"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
158
158
  ```
159
159
 
160
- ### 2d. Detect Build & Test
160
+ ### 2d. Detect Test Directory and Patterns
161
+
162
+ Check for test directories and files:
163
+
164
+ ```bash
165
+ # Check common test directories
166
+ for dir in tests test __tests__ spec src/__tests__; do
167
+ test -d "$dir" && echo "Found test directory: $dir"
168
+ done
169
+
170
+ # Check for test files (colocated pattern)
171
+ find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*_test.py" -o -name "test_*.py" -o -name "*_test.exs" -o -name "*_test.go" \) \
172
+ -not -path "*/node_modules/*" -not -path "*/.venv/*" 2>/dev/null | head -3
173
+ ```
174
+
175
+ Update config based on findings:
176
+
177
+ ```bash
178
+ # If tests/ directory found
179
+ jq '.tests.directory = "tests"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
180
+
181
+ # If test/ directory found (Elixir convention)
182
+ jq '.tests.directory = "test"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
183
+
184
+ # If colocated tests found (no test directory, but *.test.ts files exist)
185
+ jq '.tests.directory = "src"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
186
+ ```
187
+
188
+ Set test patterns based on project type:
189
+
190
+ ```bash
191
+ # Node/TypeScript projects
192
+ jq '.tests.patterns = "*.test.ts,*.test.tsx,*.spec.ts,*.spec.tsx,*.test.js,*.spec.js"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
193
+
194
+ # Python projects
195
+ jq '.tests.patterns = "*_test.py,test_*.py"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
196
+
197
+ # Elixir projects
198
+ jq '.tests.patterns = "*_test.exs"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
199
+
200
+ # Go projects
201
+ jq '.tests.patterns = "*_test.go"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
202
+ ```
203
+
204
+ **If NO tests found:**
205
+ - Say: "⚠️ No test directory found. Ralph can only verify syntax and API responses."
206
+ - Say: "Add tests, or set `checks.requireTests: false` in config to silence this warning."
207
+
208
+ ### 2e. Detect Build & Test Commands
161
209
 
162
210
  Check for build script in package.json:
163
211
  ```bash
@@ -182,6 +230,7 @@ test -f jest.config.js && echo "jest"
182
230
  test -f manage.py && echo "django"
183
231
  test -f pytest.ini && echo "pytest"
184
232
  test -f pyproject.toml && grep -q "pytest" pyproject.toml && echo "pytest"
233
+ test -f mix.exs && echo "exunit"
185
234
  ```
186
235
 
187
236
  Update config:
@@ -200,14 +249,17 @@ jq '.checks.test = "pytest"' .ralph/config.json > .ralph/config.tmp && mv .ralph
200
249
 
201
250
  # If vitest/jest found
202
251
  jq '.checks.test = "npm test"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
252
+
253
+ # If ExUnit found (Elixir)
254
+ jq '.checks.test = "mix test"' .ralph/config.json > .ralph/config.tmp && mv .ralph/config.tmp .ralph/config.json
203
255
  ```
204
256
 
205
- ### 2e. Show Results
257
+ ### 2f. Show Results
206
258
 
207
259
  After updating, read the config and show user:
208
260
 
209
261
  ```bash
210
- cat .ralph/config.json | jq '{paths, urls, commands, checks}'
262
+ cat .ralph/config.json | jq '{paths, urls, commands, checks, tests}'
211
263
  ```
212
264
 
213
265
  Say: "I've auto-configured Ralph:
@@ -216,7 +268,7 @@ Say: "I've auto-configured Ralph:
216
268
 
217
269
  Edit `.ralph/config.json` if anything needs adjusting."
218
270
 
219
- ### 2f. Test Credentials (Optional)
271
+ ### 2g. Test Credentials (Optional)
220
272
 
221
273
  ```bash
222
274
  cat .ralph/config.json | jq -r '.auth.testUser // empty'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentic-loop",
3
- "version": "3.8.0",
3
+ "version": "3.9.1",
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",