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.
- package/.claude/commands/idea.md +1 -1
- package/package.json +1 -1
- package/ralph/init.sh +83 -0
- package/ralph/utils.sh +86 -11
- package/templates/config/elixir.json +6 -0
- package/templates/config/fastmcp.json +6 -0
- package/templates/config/fullstack.json +6 -0
- package/templates/config/go.json +6 -0
- package/templates/config/minimal.json +6 -0
- package/templates/config/node.json +6 -0
- package/templates/config/python.json +6 -0
- package/templates/config/rust.json +6 -0
package/.claude/commands/idea.md
CHANGED
|
@@ -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? (
|
|
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
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
|
-
|
|
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 "$
|
|
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
|
-
#
|
|
667
|
-
|
|
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 $
|
|
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:
|
|
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:
|
|
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",
|
package/templates/config/go.json
CHANGED
|
@@ -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",
|